Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
120 changes: 89 additions & 31 deletions Tasknotes-Development-Guidelines.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,9 +99,9 @@ The `MinimalNativeCache` serves as an intelligent coordinator rather than a data
```typescript
// Only these are indexed for performance:
tasksByDate: Map<string, Set<string>> // Calendar view optimization
tasksByStatus: Map<string, Set<string>> // FilterService optimization
tasksByStatus: Map<string, Set<string>> // FilterService optimization
overdueTasks: Set<string> // Overdue query optimization

// Everything else computed on-demand:
getAllTags() -> scans native cache when called
getAllPriorities() -> scans native cache when called
Expand All @@ -112,10 +112,10 @@ The `MinimalNativeCache` serves as an intelligent coordinator rather than a data
```typescript
// Without coordination: Multiple views scan independently
CalendarView -> scans 1000 files for date tasks
AgendaView -> scans 1000 files for date tasks
AgendaView -> scans 1000 files for date tasks
KanbanView -> scans 1000 files for status tasks
// Result: 3000 file scans on data change

// With coordination: Single coordinated update
MinimalCache -> coordinates single refresh signal
All views -> refresh once with targeted data
Expand All @@ -142,7 +142,7 @@ The event system now focuses on **coordination efficiency** rather than complex
// All views receive coordinated signal
// Each view refreshes with fresh native cache data
});

// Inefficient: Direct native listening (avoided)
app.metadataCache.on('changed', (file) => {
// Each view independently processes the same change
Expand Down Expand Up @@ -176,10 +176,10 @@ The UTC Anchor principle is a fundamental architectural decision that ensures ti
```typescript
// For internal logic (comparisons, sorting, filtering)
const date = parseDateToUTC('2025-08-01'); // Always 2025-08-01T00:00:00.000Z

// For UI display (showing dates to users)
const date = parseDateToLocal('2025-08-01'); // Midnight in user's timezone

// Deprecated - do not use directly
const date = parseDate('2025-08-01'); // Legacy function, aliased to parseDateToLocal
```
Expand Down Expand Up @@ -207,13 +207,13 @@ The UTC Anchor principle is a fundamental architectural decision that ensures ti
* **For comparisons**: Use the provided safe functions that implement UTC anchors: `isSameDateSafe`, `isBeforeDateSafe`, `isOverdueTimeAware`.
* **For storage**: Always store dates in YYYY-MM-DD format using `formatDateForStorage()`.
* **For timestamps**: When creating `dateCreated` or `dateModified`, use `getCurrentTimestamp()` for timezone-aware ISO strings.

* **Migration Pattern**: When updating existing code:
```typescript
// Old pattern (fragile)
const date = parseDate(task.due);
if (isBefore(date, today)) { ... }

// New pattern (robust)
const date = parseDateToUTC(task.due); // For logic
if (isBeforeDateSafe(task.due, getTodayString())) { ... }
Expand All @@ -229,7 +229,7 @@ The UTC Anchor principle is a fundamental architectural decision that ensures ti
async refreshTasks(): Promise<void> {
// Get task paths from minimal cache (indexed)
const taskPaths = this.plugin.cacheManager.getTasksForDate(dateStr);

// Get fresh task data from native cache
const tasks = await Promise.all(
taskPaths.map(path => {
Expand All @@ -238,7 +238,7 @@ The UTC Anchor principle is a fundamental architectural decision that ensures ti
return this.extractTaskInfo(path, metadata.frontmatter);
})
);

this.renderTasks(tasks);
}
```
Expand Down Expand Up @@ -289,6 +289,64 @@ Let's say you want to add a `complexity: 'simple' | 'medium' | 'hard'` property

**Note**: With the minimal cache approach, most new properties should be computed on-demand rather than indexed. Only add indexes for frequently-accessed, performance-critical queries.

### 5.1.1. Adding a Computed Property That Needs Bases Integration

Some properties are calculated dynamically from task data (like `progress` from checkboxes in task body) but need to be available in Bases views for sorting and filtering. **Important**: For Bases compatibility, these properties must be persisted in frontmatter, not just computed on-demand.

**Why Frontmatter Persistence is Required:**
* Bases standard views (Table, etc.) require persistent values in frontmatter for sorting and filtering
* Computed properties without frontmatter storage don't work reliably in Bases
* The value must be recalculated and updated whenever the source data changes

**Example: Progress Property Implementation**

The `progress` property calculates completion percentage from top-level checkboxes in the task body and stores it in frontmatter as `task_progress` (percentage number 0-100).

1. **Update `types.ts`**: Define the computed property interface (e.g., `ProgressInfo`) and add it to `TaskInfo` as optional (e.g., `progress?: ProgressInfo`).

2. **Add to FieldMapping**: Add the property to `FieldMapping` interface and `DEFAULT_FIELD_MAPPING` in `settings/defaults.ts` (e.g., `progress: "task_progress"`).

3. **Update `FieldMapper.ts`**:
* Add reading logic in `mapFromFrontmatter()` to read the persisted value from frontmatter
* Add writing logic in `mapToFrontmatter()` to write the calculated value to frontmatter

4. **Create a Service (if computation is complex)**: Create a new service class (e.g., `ProgressService`) in `/services/` with calculation logic and caching. Add cache invalidation methods (`clearCache()`, `clearCacheForTask(path)`).

5. **Integrate Service in `main.ts`**: Add service instance to plugin class, initialize in `onload()`, and add cache invalidation in `notifyDataChanged()`.

6. **Update `TaskService.ts`**:
* In `updateTask()`: Calculate and persist the property when source data changes (e.g., when `details` changes, recalculate progress and update frontmatter)
* In `createTask()`: Calculate and persist the property when creating new tasks if source data is present

7. **Add File Change Listener**: In `main.ts`, add a listener (`metadataCache.on('changed')`) that recalculates and updates the property when files are modified directly (e.g., checkbox toggled in editor). Use debouncing to avoid excessive file writes.

8. **Add Helper Function in `utils/helpers.ts`**: Create a helper function that delegates to the service (e.g., `calculateTaskProgress()`).

9. **Update `utils/propertyHelpers.ts`**: Add the property to `coreProperties` array so it appears in property selection.

10. **Implement Rendering in `ui/TaskCard.ts`**: Add rendering function (e.g., `renderProgressProperty`). Use lazy loading: only calculate when property is visible. Load `task.details` if needed using `vault.read()` (body content isn't in metadataCache). Handle empty states gracefully.

11. **Integrate with Bases Views**:
* Register in `BasesViewBase.registerComputedProperties()`: Read from frontmatter first, fallback to cache if needed. Return the persisted value for Bases compatibility.
* Update `BasesDataAdapter.extractEntryProperties()`: Map the frontmatter property (e.g., `task_progress`) to Bases property name (e.g., `task.progress`) so Bases can discover and use it.

12. **Add Settings Integration (optional)**: If the property has display options, add settings interface, add to `TaskNotesSettings`, add defaults in `settings/defaults.ts`, add UI in settings tab, and add settings change listener in views to trigger re-renders.

13. **Add i18n Translations**: Add translations for property name in all language files, and for settings UI if applicable.

**Key Implementation Details:**
* **Frontmatter Persistence**: The calculated value must be stored in frontmatter for Bases compatibility
* **Automatic Updates**: The value must be recalculated and updated whenever source data changes (via `updateTask()`, file change listener, etc.)
* **FieldMapper Integration**: Use `FieldMapper` to read/write the persisted value with user-configurable field names
* **Service-based Calculation**: Complex computations should be encapsulated in a dedicated service with caching
* **Debounced File Writes**: File change listeners should use debouncing to avoid excessive writes

**Performance Considerations:**
* Use lazy loading for UI rendering: Only calculate when property is visible
* Implement caching: Use service-level cache with hash-based invalidation for expensive computations
* Debounce file writes: File change listeners should debounce updates (e.g., 500ms) to batch rapid changes
* Cache invalidation: Clear cache on task updates via `notifyDataChanged()`

### 5.2. Adding View-Specific Options to Saved Views

The plugin supports capturing and restoring view-specific display options as part of saved views. This feature allows views to preserve their display preferences (toggles, visibility options) alongside filter configurations.
Expand All @@ -300,7 +358,7 @@ The plugin supports capturing and restoring view-specific display options as par
export class MyView extends ItemView {
private showOption1: boolean = true;
private showOption2: boolean = false;

private setupViewOptions(): void {
// Configure FilterBar with view options
this.filterBar.setViewOptions([
Expand All @@ -318,7 +376,7 @@ private setupEventListeners(): void {
this.filterBar.on('saveView', ({ name, query, viewOptions }) => {
this.plugin.viewStateManager.saveView(name, query, viewOptions);
});

// Apply loaded view options
this.filterBar.on('loadViewOptions', (viewOptions: {[key: string]: boolean}) => {
this.applyViewOptions(viewOptions);
Expand All @@ -333,10 +391,10 @@ private applyViewOptions(viewOptions: {[key: string]: boolean}): void {
if (viewOptions.hasOwnProperty('showOption2')) {
this.showOption2 = viewOptions.showOption2;
}

// Update FilterBar to reflect loaded state
this.setupViewOptions();

// Refresh view to apply changes
this.refresh();
}
Expand Down Expand Up @@ -381,7 +439,7 @@ saveView(name: string, query: FilterQuery, viewOptions?: {[key: string]: boolean

**Advanced Calendar View Options:**
- `showScheduled`: Display tasks with scheduled dates
- `showDue`: Display tasks with due dates
- `showDue`: Display tasks with due dates
- `showTimeblocks`: Display time-blocking entries
- `showRecurring`: Display recurring task events
- `showICSEvents`: Display imported calendar events
Expand Down Expand Up @@ -539,11 +597,11 @@ async onload() {
// Essential initialization only
await this.loadSettings();
this.initializeLightweightServices();

// Register view types and commands
this.registerViews();
this.addCommands();

// Defer expensive operations
this.app.workspace.onLayoutReady(() => {
this.initializeAfterLayoutReady();
Expand All @@ -553,10 +611,10 @@ async onload() {
private async initializeAfterLayoutReady() {
// Minimal cache initialization (lightweight)
this.cacheManager.initialize();

// Heavy service initialization
await this.pomodoroService.initialize();

// Editor services with async imports
const { TaskLinkDetectionService } = await import('./services/TaskLinkDetectionService');
}
Expand Down Expand Up @@ -586,7 +644,7 @@ getTaskInfo(path: string): TaskInfo | null {
getAllTasksOfStatus(status: string): TaskInfo[] {
// Use essential index for paths
const taskPaths = this.minimalCache.getTaskPathsByStatus(status);

// Batch native cache access
return taskPaths.map(path => this.getTaskInfo(path)).filter(Boolean);
}
Expand All @@ -606,9 +664,9 @@ getAllTasksOfStatus(status: string): TaskInfo[] {
class MinimalNativeCache {
// Only 3 essential indexes (~70% reduction from previous approach)
private tasksByDate: Map<string, Set<string>> = new Map();
private tasksByStatus: Map<string, Set<string>> = new Map();
private tasksByStatus: Map<string, Set<string>> = new Map();
private overdueTasks: Set<string> = new Set();

// No redundant data storage - everything comes from native cache
getTaskInfo(path) {
return this.extractFromNativeCache(path); // Always fresh
Expand All @@ -634,15 +692,15 @@ export class MyView extends ItemView {
this.plugin = plugin;
// Lightweight constructor only
}

async onOpen() {
// Wait for plugin readiness
await this.plugin.onReady();

// Initialize view with native cache access
this.initializeView();
}

private async refreshData() {
// Direct native cache access for fresh data
const taskPaths = this.plugin.cacheManager.getTasksForDate(this.selectedDate);
Expand Down Expand Up @@ -682,14 +740,14 @@ interface ICSSubscription {
```typescript
async fetchSubscription(id: string): Promise<void> {
const subscription = this.getSubscription(id);

let icsData: string;
if (subscription.type === 'remote') {
icsData = await this.fetchRemoteICS(subscription.url);
} else {
icsData = await this.readLocalICS(subscription.filePath);
}

const events = this.parseICS(icsData);
this.updateCache(id, events);
}
Expand All @@ -708,7 +766,7 @@ private startFileWatcher(subscription: ICSSubscription): void {
setTimeout(() => this.refreshSubscription(subscription.id), 1000);
}
});

// Store cleanup function
this.fileWatchers.set(subscription.id, () => {
this.plugin.app.vault.offref(modifyRef);
Expand Down Expand Up @@ -878,7 +936,7 @@ const displayDate = hasTimeComponent(task.due)
: format(parseDateToLocal(task.due), 'MMM d');

// Pattern 3: Filtering by date
const todayTasks = tasks.filter(task =>
const todayTasks = tasks.filter(task =>
task.due && isSameDateSafe(task.due, getTodayString())
);

Expand All @@ -904,4 +962,4 @@ const updatedTask = {
- ❌ Use deprecated `parseDate()` function
- ❌ Mix parsing methods in the same logic flow
- ❌ Assume timezone behavior without testing
- ❌ Compare Date objects from different parsing methods
- ❌ Compare Date objects from different parsing methods
6 changes: 6 additions & 0 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,12 @@ ICS export allows other systems to access task data with automatic updates. The

See [Calendar Integration](features/calendar-integration.md) for details.

## Progress Tracking

TaskNotes automatically calculates task completion progress based on first-level checkboxes in the task body. A visual progress bar displays completion percentage, similar to Trello. Progress is available in Bases views for sorting and filtering.

See [Progress Tracking](features/progress-tracking.md) for details.

## User Fields

Custom fields extend task structure with additional data. These fields work in filtering, sorting, and templates.
Expand Down
Loading