mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Use official Obsidian Bases value rendering
This commit is contained in:
parent
1f6da1d408
commit
490faab1bc
22 changed files with 380 additions and 350 deletions
|
|
@ -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.
|
||||
|
|
|
|||
63
package-lock.json
generated
63
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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<BasesView, "config" | "data">;
|
||||
|
||||
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<string, unknown>;
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
/**
|
||||
* 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<string, unknown> {
|
||||
private extractEntryProperties(entry: BasesEntry): Record<string, unknown> {
|
||||
// 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;
|
||||
|
|
|
|||
|
|
@ -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<void> {
|
||||
async createFileForView(
|
||||
baseFileName?: string,
|
||||
frontmatterProcessor?: (frontmatter: any) => void
|
||||
): Promise<void> {
|
||||
const { TaskCreationModal } = await import("../modals/TaskCreationModal");
|
||||
|
||||
// Extract any default values from the frontmatter processor if provided
|
||||
|
|
|
|||
|
|
@ -479,6 +479,11 @@ export class CalendarView extends BasesViewBase {
|
|||
return defaultValue;
|
||||
}
|
||||
|
||||
private getConfigOption<T>(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<string | undefined>('slotMinTime', undefined),
|
||||
this.viewOptions.slotMinTime,
|
||||
false
|
||||
);
|
||||
this.viewOptions.slotMaxTime = this.validateTimeValue(
|
||||
this.config.get('slotMaxTime'),
|
||||
this.getConfigOption<string | undefined>('slotMaxTime', undefined),
|
||||
this.viewOptions.slotMaxTime,
|
||||
true // Allow 24:00 for end time
|
||||
);
|
||||
this.viewOptions.slotDuration = this.validateTimeValue(
|
||||
this.config.get('slotDuration'),
|
||||
this.getConfigOption<string | undefined>('slotDuration', undefined),
|
||||
this.viewOptions.slotDuration,
|
||||
false
|
||||
);
|
||||
this.viewOptions.scrollTime = this.validateTimeValue(
|
||||
this.config.get('scrollTime'),
|
||||
this.getConfigOption<string | undefined>('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<number | undefined>('eventMaxStack', undefined);
|
||||
if (eventMaxStackValue !== undefined) {
|
||||
this.viewOptions.eventMaxStack = eventMaxStackValue === 0 ? null : eventMaxStackValue;
|
||||
}
|
||||
|
||||
const dayMaxEventsValue = this.config.get('dayMaxEvents');
|
||||
const dayMaxEventsValue = this.getConfigOption<number | undefined>('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<number | undefined>('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');
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<string, BasesViewRegistration>;
|
||||
|
|
@ -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}`
|
||||
|
|
|
|||
|
|
@ -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<vo
|
|||
key: "expandedRelationshipFilterMode",
|
||||
displayName: "Expanded relationships",
|
||||
default: "inherit",
|
||||
options: ["inherit", "show-all"],
|
||||
options: {
|
||||
inherit: "Inherit",
|
||||
"show-all": "Show all",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -109,7 +113,7 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise<vo
|
|||
default: false,
|
||||
},
|
||||
{
|
||||
type: "multitext",
|
||||
type: "text",
|
||||
key: "columnOrder",
|
||||
displayName: "Column Order (Advanced)",
|
||||
placeholder: "Auto-managed when dragging columns",
|
||||
|
|
@ -120,7 +124,10 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise<vo
|
|||
key: "expandedRelationshipFilterMode",
|
||||
displayName: "Expanded relationships",
|
||||
default: "inherit",
|
||||
options: ["inherit", "show-all"],
|
||||
options: {
|
||||
inherit: "Inherit",
|
||||
"show-all": "Show all",
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
|
@ -134,7 +141,7 @@ export async function registerBasesTaskList(plugin: TaskNotesPlugin): Promise<vo
|
|||
const calendarSettings = plugin.settings.calendarViewSettings;
|
||||
const t = (key: string) => 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<vo
|
|||
const subscriptions = plugin.icsSubscriptionService.getSubscriptions();
|
||||
if (subscriptions.length > 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<vo
|
|||
const availableCalendars = plugin.googleCalendarService.getAvailableCalendars();
|
||||
if (availableCalendars.length > 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<vo
|
|||
const availableCalendars = plugin.microsoftCalendarService.getAvailableCalendars();
|
||||
if (availableCalendars.length > 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,
|
||||
|
|
|
|||
|
|
@ -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<BasesValueLike["file"]>;
|
||||
frontmatter?: Record<string, unknown>;
|
||||
properties?: Record<string, unknown>;
|
||||
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;
|
||||
}
|
||||
55
src/types/obsidian-1.11.d.ts
vendored
55
src/types/obsidian-1.11.d.ts
vendored
|
|
@ -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<T extends BaseComponent>(cb: (el: HTMLElement) => T): this;
|
||||
}
|
||||
|
||||
interface SettingTab {
|
||||
/**
|
||||
* The icon to display in the settings sidebar
|
||||
* @since Obsidian 1.11.0
|
||||
*/
|
||||
icon?: IconName;
|
||||
}
|
||||
}
|
||||
34
src/types/obsidian-1.12-cli.d.ts
vendored
34
src/types/obsidian-1.12-cli.d.ts
vendored
|
|
@ -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<string, CliFlag>;
|
||||
export type CliHandler = (params: CliData) => string | Promise<string>;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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<PropertyEventCardOptions> = {}
|
||||
): 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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,16 +1,57 @@
|
|||
import type { RenderContext, Value } from "obsidian";
|
||||
|
||||
export interface TaskCardPresentationOptions {
|
||||
propertyLabels?: Record<string, string>;
|
||||
}
|
||||
|
||||
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<string, unknown>;
|
||||
|
||||
|
|
|
|||
|
|
@ -11,6 +11,18 @@ export interface ClickHandlerOptions {
|
|||
contextMenuHandler?: (e: MouseEvent) => Promise<void>; // 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<typeof setTimeout> | 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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue