diff --git a/docs/releases/unreleased.md b/docs/releases/unreleased.md index 2f85c6b9..7f3247f5 100644 --- a/docs/releases/unreleased.md +++ b/docs/releases/unreleased.md @@ -33,8 +33,17 @@ Example: - (#1777) Added a Modal Fields setting to choose whether Tab/Shift+Tab move focus out of the task details editor or use the markdown editor's indentation behavior. - Thanks to @P-Sc for the PR. +## Changed + +- Updated the Obsidian API dependency to 1.12.3 and switched the Bases integration to the official API type declarations. +- Bases task cards and property-event cards now render metadata values through Obsidian's native Bases value renderer so links, dates, lists, icons, and formula values display more faithfully. + ## Fixed +- (#1035) Fixed the task modal title field causing the modal to jump to the bottom when focused on iPhone. + - Thanks to @3zra47 for reporting and following up. +- (#1045) Fixed inline linked recurring tasks keeping the previous day's completed state after the calendar day changes. + - Thanks to @3zra47 for reporting and following up. - (#1788) Fixed Task List subgroup headers and cards rendering without clear indentation. - Thanks to @renatomen for the PR. - (#1810) Updated Japanese Kanban terminology to use the more natural katakana spelling. @@ -68,3 +77,4 @@ Example: - (#1744) Fixed Bases Task List views so changing the per-view `Expanded relationships` option takes effect on re-render - Restores the expected `show-all` behavior for setups that hide subtasks at the top level with `note.projects.isEmpty()` - Thanks to @minol-dev for reporting and @stanley-910 for PR #1792 exploring the related standalone-subtask workflow +- Fixed tag clicks in task cards opening the task edit modal in addition to the tag search panel, and kept rendered tag metadata on a single row inside task cards. diff --git a/package-lock.json b/package-lock.json index 50f9ad6a..26920b04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "4.5.1", "license": "MIT", "dependencies": { - "@codemirror/view": "^6.37.2", + "@codemirror/view": "^6.38.6", "@fullcalendar/core": "^6.1.17", "@fullcalendar/daygrid": "^6.1.17", "@fullcalendar/interaction": "^6.1.17", @@ -30,7 +30,7 @@ }, "devDependencies": { "@codemirror/autocomplete": "^6.19.1", - "@codemirror/state": "^6.5.2", + "@codemirror/state": "^6.5.0", "@electron/asar": "^3.4.1", "@playwright/test": "^1.57.0", "@types/jest": "^30.0.0", @@ -48,7 +48,7 @@ "jest": "^30.1.1", "jest-environment-jsdom": "^30.1.1", "monkey-around": "^3.0.0", - "obsidian": "latest", + "obsidian": "^1.12.3", "obsidian-typings": "^4.49.0", "prettier": "^3.6.2", "ts-jest": "^29.1.1", @@ -756,18 +756,18 @@ } }, "node_modules/@codemirror/state": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", - "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.0.tgz", + "integrity": "sha512-MwBHVK60IiIHDcoMet78lxt6iw5gJOGSbNbOIVBHWVXIH4/Nq1+GQgLLGgI1KlnN86WDXsPudVaqYHKBIx7Eyw==", "license": "MIT", "dependencies": { "@marijn/find-cluster-break": "^1.0.0" } }, "node_modules/@codemirror/view": { - "version": "6.38.1", - "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.1.tgz", - "integrity": "sha512-RmTOkE7hRU3OVREqFVITWHz6ocgBjv08GoePscAakgVQfciA3SGCEk7mb9IzwW61cKKmlTpHXG6DUE5Ubx+MGQ==", + "version": "6.38.6", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.38.6.tgz", + "integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==", "license": "MIT", "dependencies": { "@codemirror/state": "^6.5.0", @@ -11142,9 +11142,9 @@ } }, "node_modules/obsidian": { - "version": "1.8.7", - "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", - "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.12.3.tgz", + "integrity": "sha512-HxWqe763dOqzXjnNiHmAJTRERN8KILBSqxDSEqbeSr7W8R8Jxezzbca+nz1LiiqXnMpM8lV2jzAezw3CZ4xNUw==", "dev": true, "license": "MIT", "dependencies": { @@ -11152,8 +11152,8 @@ "moment": "2.29.4" }, "peerDependencies": { - "@codemirror/state": "^6.0.0", - "@codemirror/view": "^6.0.0" + "@codemirror/state": "6.5.0", + "@codemirror/view": "6.38.6" } }, "node_modules/obsidian-daily-notes-interface": { @@ -11226,6 +11226,16 @@ "typescript": ">=4.8.0" } }, + "node_modules/obsidian-typings/node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, "node_modules/obsidian-typings/node_modules/@types/codemirror": { "version": "5.60.16", "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.16.tgz", @@ -11236,6 +11246,31 @@ "@types/tern": "*" } }, + "node_modules/obsidian-typings/node_modules/obsidian": { + "version": "1.8.7", + "resolved": "https://registry.npmjs.org/obsidian/-/obsidian-1.8.7.tgz", + "integrity": "sha512-h4bWwNFAGRXlMlMAzdEiIM2ppTGlrh7uGOJS6w4gClrsjc+ei/3YAtU2VdFUlCiPuTHpY4aBpFJJW75S1Tl/JA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/codemirror": "5.60.8", + "moment": "2.29.4" + }, + "peerDependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, + "node_modules/obsidian-typings/node_modules/obsidian/node_modules/@types/codemirror": { + "version": "5.60.8", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.8.tgz", + "integrity": "sha512-VjFgDF/eB+Aklcy15TtOTLQeMjTo07k7KAjql8OK5Dirr7a6sJY4T1uVBDuTVG9VEmn1uUsohOpYnVfgC6/jyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" + } + }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", diff --git a/package.json b/package.json index 930cfd30..86d5e514 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "license": "MIT", "devDependencies": { "@codemirror/autocomplete": "^6.19.1", - "@codemirror/state": "^6.5.2", + "@codemirror/state": "^6.5.0", "@electron/asar": "^3.4.1", "@playwright/test": "^1.57.0", "@types/jest": "^30.0.0", @@ -63,7 +63,7 @@ "jest": "^30.1.1", "jest-environment-jsdom": "^30.1.1", "monkey-around": "^3.0.0", - "obsidian": "latest", + "obsidian": "^1.12.3", "obsidian-typings": "^4.49.0", "prettier": "^3.6.2", "ts-jest": "^29.1.1", @@ -71,7 +71,7 @@ "typescript": "^5.9.2" }, "dependencies": { - "@codemirror/view": "^6.37.2", + "@codemirror/view": "^6.38.6", "@fullcalendar/core": "^6.1.17", "@fullcalendar/daygrid": "^6.1.17", "@fullcalendar/interaction": "^6.1.17", diff --git a/src/bases/BasesDataAdapter.ts b/src/bases/BasesDataAdapter.ts index 55a71ae3..bf6cd45f 100644 --- a/src/bases/BasesDataAdapter.ts +++ b/src/bases/BasesDataAdapter.ts @@ -1,12 +1,33 @@ import { BasesDataItem } from "./helpers"; -import { BasesEntryLike, BasesGroupLike, BasesValueLike, BasesViewLike } from "./types"; +import type { BasesEntry, BasesEntryGroup, BasesPropertyId, BasesView, TFile, Value } from "obsidian"; + +type BasesViewDataSource = Pick; + +type BasesValueInternals = Value & { + data?: unknown; + date?: Date; + file?: TFile; + value?: unknown[]; + get?: (index: number) => Value | unknown; + at?: (index: number) => Value | unknown; + length?: () => number; + toISOString?: () => string; + constructor?: { + name?: string; + }; +}; + +type BasesEntryInternals = BasesEntry & { + frontmatter?: Record; + properties?: Record; +}; /** * Adapter for accessing Bases data using public API (1.10.0+). * Eliminates all internal API dependencies. */ export class BasesDataAdapter { - constructor(private basesView: BasesViewLike) {} + constructor(private basesView: BasesViewDataSource) {} /** * Extract all data items from Bases query result. @@ -18,7 +39,7 @@ export class BasesDataAdapter { */ extractDataItems(): BasesDataItem[] { const entries = this.basesView.data.data; - return entries.map((entry: BasesEntryLike) => ({ + return entries.map((entry: BasesEntry) => ({ key: entry.file.path, data: entry, file: entry.file, @@ -34,7 +55,7 @@ export class BasesDataAdapter { * * Note: Returns pre-grouped data. Bases has already applied groupBy configuration. */ - getGroupedData(): BasesGroupLike[] { + getGroupedData(): BasesEntryGroup[] { return this.basesView.data.groupedData; } @@ -82,16 +103,16 @@ export class BasesDataAdapter { * Uses public API: basesView.config.getDisplayName() */ getPropertyDisplayName(propertyId: string): string { - return this.basesView.config.getDisplayName(propertyId); + return this.basesView.config.getDisplayName(propertyId as BasesPropertyId); } /** * Get property value from a Bases entry. * Uses public API: entry.getValue() */ - getPropertyValue(entry: BasesEntryLike, propertyId: string): unknown { + getPropertyValue(entry: BasesEntry, propertyId: string): unknown { try { - const value = entry.getValue(propertyId); + const value = entry.getValue(propertyId as BasesPropertyId); return this.convertValueToNative(value); } catch (e) { console.warn(`[BasesDataAdapter] Failed to get property ${propertyId}:`, e); @@ -104,8 +125,8 @@ export class BasesDataAdapter { * Convert Bases Value object to native JavaScript value. * Handles: PrimitiveValue, ListValue, DateValue, FileValue, NullValue, etc. */ - private convertValueToNative(value: BasesValueLike | unknown): unknown { - const basesValue = value as BasesValueLike | null | undefined; + private convertValueToNative(value: Value | unknown): unknown { + const basesValue = value as BasesValueInternals | null | undefined; if (basesValue == null || basesValue.constructor?.name === "NullValue") { return null; } @@ -116,11 +137,16 @@ export class BasesDataAdapter { } // ListValue - if (typeof basesValue.length === "function" && typeof basesValue.at === "function") { + const getListItem = typeof basesValue.get === "function" + ? basesValue.get.bind(basesValue) + : typeof basesValue.at === "function" + ? basesValue.at.bind(basesValue) + : null; + if (typeof basesValue.length === "function" && getListItem) { const len = basesValue.length(); const result = []; for (let i = 0; i < len; i++) { - const item = basesValue.at(i); + const item = getListItem(i); result.push(this.convertValueToNative(item)); } return result; @@ -143,7 +169,14 @@ export class BasesDataAdapter { } // Fallback: try to extract raw data - return basesValue; + if (typeof basesValue.toString === "function") { + const stringValue = basesValue.toString(); + if (stringValue !== "[object Object]") { + return stringValue; + } + } + + return value; } /** @@ -151,10 +184,10 @@ export class BasesDataAdapter { * Handles Bases Value objects, particularly DateValue which has special structure. * For FileValue (links), returns the file path which can be rendered as a clickable link. */ - convertGroupKeyToString(key: BasesValueLike | unknown): string { - const basesKey = key as BasesValueLike | null | undefined; + convertGroupKeyToString(key: Value | unknown): string { + const basesKey = key as BasesValueInternals | null | undefined; // Check if key exists and is valid - if (basesKey == null || (basesKey.hasKey && !basesKey.hasKey())) { + if (basesKey == null || basesKey.constructor?.name === "NullValue") { return "Unknown"; } @@ -210,11 +243,12 @@ export class BasesDataAdapter { * Extracts frontmatter and basic file properties only (cheap operations). * Computed file properties (backlinks, links, etc.) are fetched lazily via getComputedProperty(). */ - private extractEntryProperties(entry: BasesEntryLike): Record { + private extractEntryProperties(entry: BasesEntry): Record { // Extract all properties from the entry's frontmatter // We don't filter by visible properties here - that happens during rendering // This ensures all properties are available for TaskInfo creation - const frontmatter = entry.frontmatter || entry.properties || {}; + const entryInternals = entry as BasesEntryInternals; + const frontmatter = entryInternals.frontmatter || entryInternals.properties || {}; // Start with frontmatter properties const result = { ...frontmatter }; @@ -249,11 +283,11 @@ export class BasesDataAdapter { * Call this during rendering for visible items only - NOT during bulk extraction. * This is much more efficient for expensive properties like backlinks. */ - getComputedProperty(basesEntry: BasesEntryLike | null | undefined, propertyId: string): unknown { + getComputedProperty(basesEntry: BasesEntry | null | undefined, propertyId: string): unknown { if (!basesEntry) return null; try { - const value = basesEntry.getValue(propertyId); + const value = basesEntry.getValue(propertyId as BasesPropertyId); return this.convertValueToNative(value); } catch (e) { return null; diff --git a/src/bases/BasesViewBase.ts b/src/bases/BasesViewBase.ts index 2033fdcf..9cad2d14 100644 --- a/src/bases/BasesViewBase.ts +++ b/src/bases/BasesViewBase.ts @@ -1,4 +1,5 @@ -import { Component, App, setIcon } from "obsidian"; +import { Component, App, setIcon } from "obsidian"; +import type { BasesPropertyId, BasesQueryResult, BasesView, BasesViewConfig } from "obsidian"; import TaskNotesPlugin from "../main"; import { BasesDataAdapter } from "./BasesDataAdapter"; import { PropertyMappingService } from "./PropertyMappingService"; @@ -9,19 +10,19 @@ import { SearchBox } from "./components/SearchBox"; import { TaskSearchFilter } from "./TaskSearchFilter"; import { BatchContextMenu } from "../components/BatchContextMenu"; import type { TaskCardOptions } from "../ui/TaskCard"; -import { BasesConfigLike, BasesQueryResultLike } from "./types"; - -/** - * Abstract base class for all TaskNotes Bases views. - * Properly extends Component to leverage lifecycle, and implements BasesView interface. - * Note: Bases types (BasesView, BasesViewConfig) are available from obsidian-api declarations. - */ -export abstract class BasesViewBase extends Component { + +/** + * Abstract base class for all TaskNotes Bases views. + * Properly extends Component to leverage lifecycle, and implements BasesView interface. + * Note: Bases types (BasesView, BasesViewConfig) are available from obsidian-api declarations. + */ +export abstract class BasesViewBase extends Component implements BasesView { // BasesView properties (provided by Bases when factory returns this instance) // These match the BasesView interface from Obsidian's internal Bases API app!: App; - config!: BasesConfigLike; - data!: BasesQueryResultLike; + config!: BasesViewConfig; + data!: BasesQueryResult; + allProperties!: BasesPropertyId[]; protected plugin: TaskNotesPlugin; protected dataAdapter: BasesDataAdapter; protected propertyMapper: PropertyMappingService; @@ -355,10 +356,10 @@ export abstract class BasesViewBase extends Component { * @param baseFileName - Suggested filename from Bases (typically unused in TaskNotes) * @param frontmatterProcessor - Optional callback that Bases uses to set default frontmatter values */ - async createFileForView( - baseFileName: string, - frontmatterProcessor?: (frontmatter: any) => void - ): Promise { + async createFileForView( + baseFileName?: string, + frontmatterProcessor?: (frontmatter: any) => void + ): Promise { const { TaskCreationModal } = await import("../modals/TaskCreationModal"); // Extract any default values from the frontmatter processor if provided diff --git a/src/bases/CalendarView.ts b/src/bases/CalendarView.ts index 22fafca2..b28397fd 100644 --- a/src/bases/CalendarView.ts +++ b/src/bases/CalendarView.ts @@ -479,6 +479,11 @@ export class CalendarView extends BasesViewBase { return defaultValue; } + private getConfigOption(key: string, fallback: T): T { + const value = this.config.get(key); + return value === null || value === undefined ? fallback : (value as T); + } + /** * Read event toggle options from config. * These should be re-read on every render to respond to toggle changes. @@ -490,20 +495,20 @@ export class CalendarView extends BasesViewBase { } try { - this.viewOptions.showScheduled = this.config.get('showScheduled') ?? this.viewOptions.showScheduled; - this.viewOptions.showDue = this.config.get('showDue') ?? this.viewOptions.showDue; - this.viewOptions.showScheduledToDueSpan = this.config.get('showScheduledToDueSpan') ?? this.viewOptions.showScheduledToDueSpan; - this.viewOptions.showRecurring = this.config.get('showRecurring') ?? this.viewOptions.showRecurring; - this.viewOptions.showTimeEntries = this.config.get('showTimeEntries') ?? this.viewOptions.showTimeEntries; - this.viewOptions.showTimeblocks = this.config.get('showTimeblocks') ?? this.viewOptions.showTimeblocks; - this.viewOptions.showPropertyBasedEvents = this.config.get('showPropertyBasedEvents') ?? this.viewOptions.showPropertyBasedEvents; + this.viewOptions.showScheduled = this.getConfigOption('showScheduled', this.viewOptions.showScheduled); + this.viewOptions.showDue = this.getConfigOption('showDue', this.viewOptions.showDue); + this.viewOptions.showScheduledToDueSpan = this.getConfigOption('showScheduledToDueSpan', this.viewOptions.showScheduledToDueSpan); + this.viewOptions.showRecurring = this.getConfigOption('showRecurring', this.viewOptions.showRecurring); + this.viewOptions.showTimeEntries = this.getConfigOption('showTimeEntries', this.viewOptions.showTimeEntries); + this.viewOptions.showTimeblocks = this.getConfigOption('showTimeblocks', this.viewOptions.showTimeblocks); + this.viewOptions.showPropertyBasedEvents = this.getConfigOption('showPropertyBasedEvents', this.viewOptions.showPropertyBasedEvents); // ICS calendar toggles if (this.plugin.icsSubscriptionService) { const subscriptions = this.plugin.icsSubscriptionService.getSubscriptions(); for (const sub of subscriptions) { const key = `showICS_${sub.id}`; - this.icsCalendarToggles.set(sub.id, this.config.get(key) ?? true); + this.icsCalendarToggles.set(sub.id, this.getConfigOption(key, true)); } } @@ -512,7 +517,7 @@ export class CalendarView extends BasesViewBase { const calendars = this.plugin.googleCalendarService.getAvailableCalendars(); for (const cal of calendars) { const key = `showGoogleCalendar_${cal.id}`; - this.googleCalendarToggles.set(cal.id, this.config.get(key) ?? true); + this.googleCalendarToggles.set(cal.id, this.getConfigOption(key, true)); } } @@ -521,7 +526,7 @@ export class CalendarView extends BasesViewBase { const calendars = this.plugin.microsoftCalendarService.getAvailableCalendars(); for (const cal of calendars) { const key = `showMicrosoftCalendar_${cal.id}`; - this.microsoftCalendarToggles.set(cal.id, this.config.get(key) ?? true); + this.microsoftCalendarToggles.set(cal.id, this.getConfigOption(key, true)); } } } catch (e) { @@ -544,75 +549,75 @@ export class CalendarView extends BasesViewBase { this.readEventToggles(); // Date navigation - this.viewOptions.initialDate = this.config.get('initialDate') ?? this.viewOptions.initialDate; - this.viewOptions.initialDateProperty = this.config.get('initialDateProperty') ?? this.viewOptions.initialDateProperty; - this.viewOptions.initialDateStrategy = this.config.get('initialDateStrategy') ?? this.viewOptions.initialDateStrategy; + this.viewOptions.initialDate = this.getConfigOption('initialDate', this.viewOptions.initialDate); + this.viewOptions.initialDateProperty = this.getConfigOption('initialDateProperty', this.viewOptions.initialDateProperty); + this.viewOptions.initialDateStrategy = this.getConfigOption('initialDateStrategy', this.viewOptions.initialDateStrategy); // Layout - this.viewOptions.calendarView = this.config.get('calendarView') ?? this.viewOptions.calendarView; - this.viewOptions.customDayCount = this.config.get('customDayCount') ?? this.viewOptions.customDayCount; - this.viewOptions.listDayCount = this.config.get('listDayCount') ?? this.viewOptions.listDayCount; + this.viewOptions.calendarView = this.getConfigOption('calendarView', this.viewOptions.calendarView); + this.viewOptions.customDayCount = this.getConfigOption('customDayCount', this.viewOptions.customDayCount); + this.viewOptions.listDayCount = this.getConfigOption('listDayCount', this.viewOptions.listDayCount); // Validate time values to prevent crashes from invalid input this.viewOptions.slotMinTime = this.validateTimeValue( - this.config.get('slotMinTime'), + this.getConfigOption('slotMinTime', undefined), this.viewOptions.slotMinTime, false ); this.viewOptions.slotMaxTime = this.validateTimeValue( - this.config.get('slotMaxTime'), + this.getConfigOption('slotMaxTime', undefined), this.viewOptions.slotMaxTime, true // Allow 24:00 for end time ); this.viewOptions.slotDuration = this.validateTimeValue( - this.config.get('slotDuration'), + this.getConfigOption('slotDuration', undefined), this.viewOptions.slotDuration, false ); this.viewOptions.scrollTime = this.validateTimeValue( - this.config.get('scrollTime'), + this.getConfigOption('scrollTime', undefined), this.viewOptions.scrollTime, false ); - this.viewOptions.firstDay = Number(this.config.get('firstDay') ?? this.viewOptions.firstDay); - this.viewOptions.weekNumbers = this.config.get('weekNumbers') ?? this.viewOptions.weekNumbers; - this.viewOptions.nowIndicator = this.config.get('nowIndicator') ?? this.viewOptions.nowIndicator; - this.viewOptions.showWeekends = this.config.get('showWeekends') ?? this.viewOptions.showWeekends; - this.viewOptions.showAllDaySlot = this.config.get('showAllDaySlot') ?? this.viewOptions.showAllDaySlot; - this.viewOptions.showTodayHighlight = this.config.get('showTodayHighlight') ?? this.viewOptions.showTodayHighlight; - const todayColumnWidthMultiplier = Number(this.config.get('todayColumnWidthMultiplier') ?? 1); + this.viewOptions.firstDay = Number(this.getConfigOption('firstDay', this.viewOptions.firstDay)); + this.viewOptions.weekNumbers = this.getConfigOption('weekNumbers', this.viewOptions.weekNumbers); + this.viewOptions.nowIndicator = this.getConfigOption('nowIndicator', this.viewOptions.nowIndicator); + this.viewOptions.showWeekends = this.getConfigOption('showWeekends', this.viewOptions.showWeekends); + this.viewOptions.showAllDaySlot = this.getConfigOption('showAllDaySlot', this.viewOptions.showAllDaySlot); + this.viewOptions.showTodayHighlight = this.getConfigOption('showTodayHighlight', this.viewOptions.showTodayHighlight); + const todayColumnWidthMultiplier = Number(this.getConfigOption('todayColumnWidthMultiplier', 1)); this.viewOptions.todayColumnWidthMultiplier = todayColumnWidthMultiplier >= 1 && todayColumnWidthMultiplier <= 5 ? Math.round(todayColumnWidthMultiplier * 2) / 2 : 1; - this.viewOptions.selectMirror = this.config.get('selectMirror') ?? this.viewOptions.selectMirror; - this.viewOptions.timeFormat = this.config.get('timeFormat') ?? this.viewOptions.timeFormat; - this.viewOptions.eventMinHeight = this.config.get('eventMinHeight') ?? this.viewOptions.eventMinHeight; - this.viewOptions.slotEventOverlap = this.config.get('slotEventOverlap') ?? this.viewOptions.slotEventOverlap; + this.viewOptions.selectMirror = this.getConfigOption('selectMirror', this.viewOptions.selectMirror); + this.viewOptions.timeFormat = this.getConfigOption('timeFormat', this.viewOptions.timeFormat); + this.viewOptions.eventMinHeight = this.getConfigOption('eventMinHeight', this.viewOptions.eventMinHeight); + this.viewOptions.slotEventOverlap = this.getConfigOption('slotEventOverlap', this.viewOptions.slotEventOverlap); // Convert slider values: 0 means special behavior (null/true/false) - const eventMaxStackValue = this.config.get('eventMaxStack'); + const eventMaxStackValue = this.getConfigOption('eventMaxStack', undefined); if (eventMaxStackValue !== undefined) { this.viewOptions.eventMaxStack = eventMaxStackValue === 0 ? null : eventMaxStackValue; } - const dayMaxEventsValue = this.config.get('dayMaxEvents'); + const dayMaxEventsValue = this.getConfigOption('dayMaxEvents', undefined); if (dayMaxEventsValue !== undefined) { // 0 = auto (true), positive number = limit this.viewOptions.dayMaxEvents = dayMaxEventsValue === 0 ? true : dayMaxEventsValue; } - const dayMaxEventRowsValue = this.config.get('dayMaxEventRows'); + const dayMaxEventRowsValue = this.getConfigOption('dayMaxEventRows', undefined); if (dayMaxEventRowsValue !== undefined) { // 0 = unlimited (false), positive number = limit this.viewOptions.dayMaxEventRows = dayMaxEventRowsValue === 0 ? false : dayMaxEventRowsValue; } // Property-based events - this.viewOptions.startDateProperty = this.config.get('startDateProperty') ?? this.viewOptions.startDateProperty; - this.viewOptions.endDateProperty = this.config.get('endDateProperty') ?? this.viewOptions.endDateProperty; - this.viewOptions.titleProperty = this.config.get('titleProperty') ?? this.viewOptions.titleProperty; + this.viewOptions.startDateProperty = this.getConfigOption('startDateProperty', this.viewOptions.startDateProperty); + this.viewOptions.endDateProperty = this.getConfigOption('endDateProperty', this.viewOptions.endDateProperty); + this.viewOptions.titleProperty = this.getConfigOption('titleProperty', this.viewOptions.titleProperty); // Read enableSearch toggle (default: false for backward compatibility) const enableSearchValue = this.config.get('enableSearch'); diff --git a/src/bases/KanbanView.ts b/src/bases/KanbanView.ts index 43c82a46..7525a2a0 100644 --- a/src/bases/KanbanView.ts +++ b/src/bases/KanbanView.ts @@ -3086,7 +3086,8 @@ export class KanbanView extends BasesViewBase { targetDate, hideStatusIndicator, expandedRelationshipFilterMode: this.expandedRelationshipFilterMode, - resolveExpandedRelationshipFilterMode: () => this.config?.get("expandedRelationshipFilterMode"), + resolveExpandedRelationshipFilterMode: (): "inherit" | "show-all" => + normalizeExpandedRelationshipFilterMode(this.config?.get("expandedRelationshipFilterMode")), expandedRelationshipTaskPaths: this.currentVisibleTaskPaths, }); } diff --git a/src/bases/TaskListView.ts b/src/bases/TaskListView.ts index 029dae7e..2cffa869 100644 --- a/src/bases/TaskListView.ts +++ b/src/bases/TaskListView.ts @@ -1773,7 +1773,8 @@ export class TaskListView extends BasesViewBase { return this.buildTaskCardOptions({ targetDate, expandedRelationshipFilterMode: this.expandedRelationshipFilterMode, - resolveExpandedRelationshipFilterMode: () => this.config?.get("expandedRelationshipFilterMode"), + resolveExpandedRelationshipFilterMode: (): "inherit" | "show-all" => + normalizeExpandedRelationshipFilterMode(this.config?.get("expandedRelationshipFilterMode")), expandedRelationshipTaskPaths: this.currentVisibleTaskPaths, }); } diff --git a/src/bases/api.ts b/src/bases/api.ts index ed49a1de..94df28da 100644 --- a/src/bases/api.ts +++ b/src/bases/api.ts @@ -6,6 +6,7 @@ */ import { App, Plugin } from "obsidian"; +import type { BasesViewRegistration as ObsidianBasesViewRegistration } from "obsidian"; export interface BasesQuery { on?: (event: string, callback: () => void) => void; @@ -31,82 +32,7 @@ export interface BasesContainer { }; } -// View Option Types (Obsidian 1.10.0+ Public API) -export type ViewOption = - | PropertyOption - | DropdownOption - | SliderOption - | ToggleOption - | TextOption - | MultitextOption - | GroupOption; - -export interface PropertyOption { - type: "property"; - key: string; - displayName: string; - default?: string; - placeholder?: string; - filter?: (prop: string) => boolean; -} - -export interface DropdownOption { - type: "dropdown"; - key: string; - displayName: string; - options: string[]; - default?: string; -} - -export interface SliderOption { - type: "slider"; - key: string; - displayName: string; - min: number; - max: number; - step: number; - default?: number; -} - -export interface ToggleOption { - type: "toggle"; - key: string; - displayName: string; - default?: boolean; -} - -export interface TextOption { - type: "text"; - key: string; - displayName: string; - default?: string; - placeholder?: string; -} - -export interface MultitextOption { - type: "multitext"; - key: string; - displayName: string; - default?: string; - placeholder?: string; -} - -export interface GroupOption { - type: "group"; - displayName: string; - options: ViewOption[]; -} - -/** - * Bases View Registration interface (Obsidian 1.10.0+ Public API) - * Updated to match official BasesViewFactory signature. - */ -export interface BasesViewRegistration { - name: string; - icon: string; - factory: (controller: any, containerEl: HTMLElement) => any; - options?: () => ViewOption[]; -} +export type BasesViewRegistration = ObsidianBasesViewRegistration; export interface BasesAPI { registrations: Record; @@ -169,9 +95,9 @@ export function registerBasesView( registration: BasesViewRegistration ): boolean { // Use public API (Obsidian 1.10.0+) - if (typeof (plugin as any).registerBasesView === "function") { + if (typeof plugin.registerBasesView === "function") { try { - const success = (plugin as any).registerBasesView(viewId, registration); + const success = plugin.registerBasesView(viewId, registration); if (success) { console.debug( `[TaskNotes][Bases] Successfully registered view via public API: ${viewId}` diff --git a/src/bases/registration.ts b/src/bases/registration.ts index 5f1b7c52..81772876 100644 --- a/src/bases/registration.ts +++ b/src/bases/registration.ts @@ -1,6 +1,7 @@ /* eslint-disable no-console */ import TaskNotesPlugin from "../main"; import { requireApiVersion } from "obsidian"; +import type { BasesAllOptions, BasesOptions } from "obsidian"; import { buildTaskListViewFactory } from "./TaskListView"; import { buildKanbanViewFactory } from "./KanbanView"; import { buildCalendarViewFactory } from "./CalendarView"; @@ -45,7 +46,10 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise plugin.i18n.translate(`views.basesCalendar.settings.${key}`); - const options: any[] = [ + const options: BasesAllOptions[] = [ { type: "group", displayName: t("groups.events"), @@ -444,7 +451,7 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise 0) { // Create a group for ICS calendars - const icsToggles: any[] = subscriptions.map(sub => ({ + const icsToggles: BasesOptions[] = subscriptions.map(sub => ({ type: "toggle", key: `showICS_${sub.id}`, displayName: sub.name, @@ -465,7 +472,7 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise 0) { // Create toggles for Google calendars - const googleToggles: any[] = availableCalendars.map(cal => ({ + const googleToggles: BasesOptions[] = availableCalendars.map(cal => ({ type: "toggle", key: `showGoogleCalendar_${cal.id}`, displayName: cal.summary || cal.id, @@ -486,7 +493,7 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise 0) { // Create toggles for Microsoft calendars - const microsoftToggles: any[] = availableCalendars.map(cal => ({ + const microsoftToggles: BasesOptions[] = availableCalendars.map(cal => ({ type: "toggle", key: `showMicrosoftCalendar_${cal.id}`, displayName: cal.summary || cal.id, diff --git a/src/bases/types.ts b/src/bases/types.ts deleted file mode 100644 index 05bbc6f3..00000000 --- a/src/bases/types.ts +++ /dev/null @@ -1,55 +0,0 @@ -export interface BasesValueLike { - data?: unknown; - date?: Date; - file?: { - path: string; - name?: string; - basename?: string; - extension?: string; - stat?: { - size?: number; - ctime?: number; - mtime?: number; - }; - }; - length?: () => number; - at?: (index: number) => BasesValueLike | unknown; - hasKey?: () => boolean; - toISOString?: () => string; - constructor?: { - name?: string; - }; -} - -export interface BasesEntryLike { - file: NonNullable; - frontmatter?: Record; - properties?: Record; - getValue(propertyId: string): BasesValueLike | unknown; -} - -export interface BasesGroupLike { - hasKey(): boolean; - key?: BasesValueLike | unknown; - rows?: BasesEntryLike[]; - entries: BasesEntryLike[]; -} - -export interface BasesConfigLike { - getSort(): unknown; - getOrder(): string[]; - getDisplayName(propertyId: string): string; - get(key: string): any; - set(key: string, value: unknown): void; - getAsPropertyId(key: string): string | null; -} - -export interface BasesQueryResultLike { - data: BasesEntryLike[]; - groupedData: BasesGroupLike[]; -} - -export interface BasesViewLike { - config: BasesConfigLike; - data: BasesQueryResultLike; -} diff --git a/src/types/obsidian-1.11.d.ts b/src/types/obsidian-1.11.d.ts deleted file mode 100644 index b26e6a42..00000000 --- a/src/types/obsidian-1.11.d.ts +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Type declarations for Obsidian 1.11.0 APIs - * These augment the existing obsidian module types - */ - -import "obsidian"; - -declare module "obsidian" { - /** - * A group of related settings with a heading - * @since Obsidian 1.11.0 - */ - export class SettingGroup { - /** - * Creates a new setting group - * @param containerEl - The container element to add the group to - */ - constructor(containerEl: HTMLElement); - - /** - * Sets the heading text for the group - * @param text - The heading text or a DocumentFragment - */ - setHeading(text: string | DocumentFragment): this; - - /** - * Adds a CSS class to the group element - * @param cls - The class name to add - */ - addClass(cls: string): this; - - /** - * Adds a setting to the group - * @param cb - Callback that receives the Setting to configure - */ - addSetting(cb: (setting: Setting) => void): this; - } - - interface Setting { - /** - * Adds a custom component to the setting - * @since Obsidian 1.11.0 - * @param cb - Callback that receives the setting element and returns a component - */ - addComponent(cb: (el: HTMLElement) => T): this; - } - - interface SettingTab { - /** - * The icon to display in the settings sidebar - * @since Obsidian 1.11.0 - */ - icon?: IconName; - } -} diff --git a/src/types/obsidian-1.12-cli.d.ts b/src/types/obsidian-1.12-cli.d.ts deleted file mode 100644 index 9369f730..00000000 --- a/src/types/obsidian-1.12-cli.d.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * Type declarations for Obsidian 1.12.2 CLI APIs - * These augment the existing obsidian module types - */ - -import "obsidian"; - -declare module "obsidian" { - export interface CliData { - [key: string]: string | "true"; - } - - export interface CliFlag { - value?: string; - description: string; - required?: boolean; - } - - export type CliFlags = Record; - export type CliHandler = (params: CliData) => string | Promise; - - interface Plugin { - /** - * Register a CLI handler to handle a command from the CLI. - * @since Obsidian 1.12.2 - */ - registerCliHandler( - command: string, - description: string, - flags: CliFlags | null, - handler: CliHandler - ): void; - } -} diff --git a/src/ui/PropertyEventCard.ts b/src/ui/PropertyEventCard.ts index e8d2d442..96c2c1df 100644 --- a/src/ui/PropertyEventCard.ts +++ b/src/ui/PropertyEventCard.ts @@ -1,5 +1,7 @@ import { setIcon } from "obsidian"; +import type { BasesEntry, BasesPropertyId, BasesViewConfig } from "obsidian"; import TaskNotesPlugin from "../main"; +import { isNullBasesValue, renderBasesValue } from "./taskCardPresentation"; export interface PropertyEventCardOptions { showProperties: boolean; @@ -14,9 +16,9 @@ export const DEFAULT_PROPERTY_EVENT_CARD_OPTIONS: PropertyEventCardOptions = { * Shows file title and Bases properties configured as visible in the view */ export function createPropertyEventCard( - entry: any, // BasesEntry from Bases + entry: BasesEntry, plugin: TaskNotesPlugin, - viewConfig?: any, // BasesViewConfig + viewConfig?: BasesViewConfig, options: Partial = {} ): HTMLElement { const opts = { ...DEFAULT_PROPERTY_EVENT_CARD_OPTIONS, ...options }; @@ -65,9 +67,10 @@ export function createPropertyEventCard( // Metadata line: show visible properties from Bases view if (opts.showProperties && viewConfig) { const metadata = content.createEl("div", { cls: "task-card__metadata" }); - const parts: string[] = []; + let renderedProperties = 0; try { + const doc = metadata.ownerDocument; // Get visible properties from Bases view configuration const visibleProperties = viewConfig.getOrder?.() || []; @@ -83,30 +86,33 @@ export function createPropertyEventCard( } // Get property value from Bases entry - const value = entry.getValue?.(propertyId); + const value = entry.getValue(propertyId as BasesPropertyId); - if (value && value.data !== null && value.data !== undefined) { - // Get user-friendly property name - const displayName = viewConfig.getDisplayName?.(propertyId) || propertyId; - - // Format the value - let displayValue = String(value.data); - - // Truncate long values - if (displayValue.length > 30) { - displayValue = displayValue.substring(0, 27) + "..."; + if (!isNullBasesValue(value)) { + if (renderedProperties > 0) { + metadata.appendChild(doc.createTextNode(" • ")); } - parts.push(`${displayName}: ${displayValue}`); + // Get user-friendly property name + const displayName = viewConfig.getDisplayName(propertyId as BasesPropertyId) || propertyId; + + const propertyEl = metadata.createSpan({ + cls: "property-event-card__metadata-property", + }); + propertyEl.createSpan({ text: `${displayName}: ` }); + const valueEl = propertyEl.createSpan({ + cls: "property-event-card__metadata-value", + }); + renderBasesValue(valueEl, value, plugin.app.renderContext); + + renderedProperties++; } } } catch (error) { console.debug("[TaskNotes][PropertyEventCard] Error reading properties:", error); } - if (parts.length > 0) { - metadata.textContent = parts.join(" • "); - } else { + if (renderedProperties === 0) { // Fallback: show file path if no properties metadata.textContent = file.path; } diff --git a/src/ui/TaskCard.ts b/src/ui/TaskCard.ts index 35a3fb95..123303ae 100644 --- a/src/ui/TaskCard.ts +++ b/src/ui/TaskCard.ts @@ -37,6 +37,8 @@ import { import { DEFAULT_INTERNAL_VISIBLE_PROPERTIES } from "../settings/defaults"; import { extractBasesValue, + isNullBasesValue, + renderBasesValue, resolveTaskCardPropertyLabel, type TaskCardPresentationOptions, } from "./taskCardPresentation"; @@ -938,6 +940,7 @@ function hasValidValue(value: any): boolean { return ( value !== null && value !== undefined && + !isNullBasesValue(value) && !(Array.isArray(value) && value.length === 0) && !(typeof value === "string" && value.trim() === "") ); @@ -967,7 +970,9 @@ function renderUserProperty( element.createEl("span", { text: `${fieldName}: ` }); // Create value container - const valueContainer = element.createEl("span"); + const valueContainer = element.createEl("span", { + cls: "task-card__metadata-value", + }); // Create shared services to avoid redundant object creation const linkServices: LinkServices = { @@ -1051,7 +1056,9 @@ function renderGenericProperty( element.createEl("span", { text: `${displayName}: ` }); // Create value container - const valueContainer = element.createEl("span"); + const valueContainer = element.createEl("span", { + cls: "task-card__metadata-value", + }); if (Array.isArray(value)) { // Handle arrays - render each item separately to detect links @@ -1076,6 +1083,10 @@ function renderPropertyValue( value: unknown, plugin?: TaskNotesPlugin ): void { + if (plugin && renderBasesValue(container, value, plugin.app.renderContext)) { + return; + } + if (typeof value === "string" && plugin) { // Check if string contains links and render appropriately const linkServices: LinkServices = { diff --git a/src/ui/renderers/linkRenderer.ts b/src/ui/renderers/linkRenderer.ts index 170759c0..a7e1fd68 100644 --- a/src/ui/renderers/linkRenderer.ts +++ b/src/ui/renderers/linkRenderer.ts @@ -226,6 +226,7 @@ export function renderTextWithLinks( href: tag, role: "button", tabindex: "0", + "data-tn-click-exclude": "true", }, }); @@ -238,6 +239,7 @@ export function renderTextWithLinks( tagEl.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); + e.stopPropagation(); options.onTagClick!(tag, e as any); } }); diff --git a/src/ui/renderers/tagRenderer.ts b/src/ui/renderers/tagRenderer.ts index 48fb82b9..64d58554 100644 --- a/src/ui/renderers/tagRenderer.ts +++ b/src/ui/renderers/tagRenderer.ts @@ -19,6 +19,7 @@ export function renderTag(container: HTMLElement, tag: string, services?: TagSer href: normalized, role: "button", tabindex: "0", + "data-tn-click-exclude": "true", }, }); @@ -34,6 +35,7 @@ export function renderTag(container: HTMLElement, tag: string, services?: TagSer el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); + e.stopPropagation(); services.onTagClick!(normalized, e as any); } }); @@ -81,6 +83,7 @@ export function renderContextsValue( attr: { role: "button", tabindex: "0", + "data-tn-click-exclude": "true", }, }); @@ -94,6 +97,7 @@ export function renderContextsValue( el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); + e.stopPropagation(); services.onTagClick!(normalized, e); } }); @@ -120,6 +124,7 @@ export function renderContextsValue( attr: { role: "button", tabindex: "0", + "data-tn-click-exclude": "true", }, }); @@ -133,6 +138,7 @@ export function renderContextsValue( el.addEventListener("keydown", (e) => { if (e.key === "Enter" || e.key === " ") { e.preventDefault(); + e.stopPropagation(); services.onTagClick!(normalized, e); } }); diff --git a/src/ui/taskCardPresentation.ts b/src/ui/taskCardPresentation.ts index 73d3648e..6350e2d6 100644 --- a/src/ui/taskCardPresentation.ts +++ b/src/ui/taskCardPresentation.ts @@ -1,16 +1,57 @@ +import type { RenderContext, Value } from "obsidian"; + export interface TaskCardPresentationOptions { propertyLabels?: Record; } +export function isBasesValue(value: unknown): value is Value { + return ( + typeof value === "object" && + value !== null && + typeof (value as { renderTo?: unknown }).renderTo === "function" && + typeof (value as { toString?: unknown }).toString === "function" + ); +} + +export function isNullBasesValue(value: unknown): boolean { + return value === null || (value as { constructor?: { name?: string } } | undefined)?.constructor?.name === "NullValue"; +} + +export function renderBasesValue( + container: HTMLElement, + value: unknown, + renderContext: RenderContext +): boolean { + if (!isBasesValue(value) || isNullBasesValue(value)) { + return false; + } + + try { + value.renderTo(container, renderContext); + if (!container.hasChildNodes() && !container.textContent) { + container.textContent = value.toString(); + } + } catch (error) { + console.debug("[TaskNotes] Error rendering Bases value:", error); + container.textContent = value.toString(); + } + + return true; +} + /** - * Extract raw value from a Bases Value object. - * Bases API may return objects like {icon: "...", data: ...} or {icon: "...", link: "..."} - * instead of raw primitive values. This function extracts the actual value. - * - * For link values (icon: "lucide-link"), Bases strips the [[]] from wikilinks, - * so we need to restore them to ensure proper rendering. + * Normalize older Bases-like wrapper objects. Official Bases Value instances are + * returned intact so TaskCard can render them with Value.renderTo(). */ export function extractBasesValue(value: unknown): unknown { + if (isNullBasesValue(value)) { + return ""; + } + + if (isBasesValue(value)) { + return value; + } + if (value && typeof value === "object" && "icon" in value) { const v = value as Record; diff --git a/src/utils/clickHandlers.ts b/src/utils/clickHandlers.ts index 3f4a4747..d3c35379 100644 --- a/src/utils/clickHandlers.ts +++ b/src/utils/clickHandlers.ts @@ -11,6 +11,18 @@ export interface ClickHandlerOptions { contextMenuHandler?: (e: MouseEvent) => Promise; // Optional context menu handler } +const DEFAULT_EXCLUDE_SELECTOR = [ + "a", + "button", + "input", + "textarea", + "select", + "[role=\"button\"]", + "[data-tn-no-drag=\"true\"]", + "[data-tn-click-exclude=\"true\"]", + ".tag", +].join(", "); + /** * Creates a reusable click handler that supports single/double click distinction * based on user settings. @@ -19,6 +31,9 @@ export interface ClickHandlerOptions { export function createTaskClickHandler(options: ClickHandlerOptions) { const { task, plugin, excludeSelector, onSingleClick, onDoubleClick, contextMenuHandler } = options; + const clickExcludeSelector = excludeSelector + ? `${DEFAULT_EXCLUDE_SELECTOR}, ${excludeSelector}` + : DEFAULT_EXCLUDE_SELECTOR; let clickTimeout: ReturnType | null = null; @@ -71,11 +86,9 @@ export function createTaskClickHandler(options: ClickHandlerOptions) { }; const clickHandler = async (e: MouseEvent) => { - if (excludeSelector) { - const target = e.target as HTMLElement; - if (target.closest(excludeSelector)) { - return; - } + const target = e.target as HTMLElement; + if (target.closest(clickExcludeSelector)) { + return; } // Check for selection mode - only shift+click triggers selection diff --git a/styles/task-card-bem.css b/styles/task-card-bem.css index a528ea2f..0bf0cc76 100644 --- a/styles/task-card-bem.css +++ b/styles/task-card-bem.css @@ -119,6 +119,37 @@ white-space: nowrap; } +.tasknotes-plugin .task-card__metadata-property { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; + max-width: 100%; +} + +.tasknotes-plugin .task-card__metadata-value { + display: inline-flex; + align-items: center; + gap: 4px; + min-width: 0; + max-width: 100%; + overflow: hidden; + white-space: nowrap; +} + +.tasknotes-plugin .task-card__metadata-property:has(.tag), +.tasknotes-plugin .task-card__metadata-value:has(.tag) { + flex-wrap: nowrap; + overflow: hidden; +} + +.tasknotes-plugin .task-card__metadata-property .tag { + display: inline-flex; + align-items: center; + flex: 0 0 auto; + white-space: nowrap; +} + .tasknotes-plugin .task-card__metadata-pill { display: inline-flex; align-items: center; diff --git a/tests/unit/issues/issue-1720-bases-date-value-icon-rendering.test.ts b/tests/unit/issues/issue-1720-bases-date-value-icon-rendering.test.ts index d9806cfc..111fa5a8 100644 --- a/tests/unit/issues/issue-1720-bases-date-value-icon-rendering.test.ts +++ b/tests/unit/issues/issue-1720-bases-date-value-icon-rendering.test.ts @@ -20,6 +20,7 @@ import { describe, it, expect } from '@jest/globals'; import { extractBasesValue, + renderBasesValue, resolveTaskCardPropertyLabel, } from "../../../src/ui/taskCardPresentation"; @@ -64,4 +65,18 @@ describe('Issue #1720: Bases date-like value rendering', () => { }) ).toBe("Modified"); }); + + it('preserves official Bases Values for native rendering', () => { + const value = { + renderTo: (el: HTMLElement) => { + el.textContent = "Native Bases value"; + }, + toString: () => "Fallback Bases value", + }; + const container = document.createElement("span"); + + expect(extractBasesValue(value)).toBe(value); + expect(renderBasesValue(container, value, {} as any)).toBe(true); + expect(container.textContent).toBe("Native Bases value"); + }); }); diff --git a/tests/unit/ui/TaskCard.test.ts b/tests/unit/ui/TaskCard.test.ts index 85f7df62..58c1e2eb 100644 --- a/tests/unit/ui/TaskCard.test.ts +++ b/tests/unit/ui/TaskCard.test.ts @@ -593,6 +593,35 @@ describe('TaskCard Component', () => { expect(mockPlugin.openTaskEditModal).toHaveBeenCalledWith(task); }); + it('should not open the edit modal when clicking a Bases-rendered tag', async () => { + const basesTagsValue = { + renderTo: (el: HTMLElement) => { + el.createEl('a', { + cls: 'tag', + text: '#client', + attr: { href: '#client' }, + }); + }, + toString: () => '#client', + }; + const tagTask = TaskFactory.createTask({ + customProperties: { + 'file.tags': basesTagsValue, + }, + }); + mockPlugin.fieldMapper.lookupMappingKey = jest.fn(() => null); + const tagCard = createTaskCard(tagTask, mockPlugin, ['file.tags']); + container.appendChild(tagCard); + + const tag = tagCard.querySelector('.tag') as HTMLElement; + expect(tag).toBeTruthy(); + + tag.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })); + await new Promise(resolve => setTimeout(resolve, 10)); + + expect(mockPlugin.openTaskEditModal).not.toHaveBeenCalled(); + }); + it('should handle Ctrl+click to open source note', async () => { const mockFile = new TFile('test.md'); mockApp.vault.getAbstractFileByPath.mockReturnValue(mockFile);