Implement complete Obsidian Task Export Plugin

Co-authored-by: RaoulJacobs <18635340+RaoulJacobs@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot] 2025-11-14 11:13:13 +00:00
parent b5292f6d23
commit 883a134221
17 changed files with 3456 additions and 0 deletions

22
.gitignore vendored Normal file
View file

@ -0,0 +1,22 @@
# Node modules
node_modules/
# Build output
main.js
main.js.map
*.js.map
# OS files
.DS_Store
Thumbs.db
# Editor files
.vscode/
.idea/
# npm package files
*.tgz
# Temporary files
*.tmp
*.log

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2024 Raoul Jacobs
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

48
esbuild.config.mjs Normal file
View file

@ -0,0 +1,48 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
const banner =
`/*
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
if you want to view the source, please visit the github repository of this plugin
*/
`;
const prod = (process.argv[2] === 'production');
const context = await esbuild.context({
banner: {
js: banner,
},
entryPoints: ['src/main.ts'],
bundle: true,
external: [
'obsidian',
'electron',
'@codemirror/autocomplete',
'@codemirror/collab',
'@codemirror/commands',
'@codemirror/language',
'@codemirror/lint',
'@codemirror/search',
'@codemirror/state',
'@codemirror/view',
'@lezer/common',
'@lezer/highlight',
'@lezer/lr',
...builtins],
format: 'cjs',
target: 'es2018',
logLevel: "info",
sourcemap: prod ? false : 'inline',
treeShaking: true,
outfile: 'main.js',
});
if (prod) {
await context.rebuild();
process.exit(0);
} else {
await context.watch();
}

10
manifest.json Normal file
View file

@ -0,0 +1,10 @@
{
"id": "task-export-plugin",
"name": "Task Export Tool",
"version": "1.0.0",
"minAppVersion": "1.0.0",
"description": "Export outstanding tasks to CSV for time tracking integration",
"author": "Raoul Jacobs",
"authorUrl": "https://github.com/tailormade-eu",
"isDesktopOnly": false
}

2406
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

31
package.json Normal file
View file

@ -0,0 +1,31 @@
{
"name": "obsidian-task-export-plugin",
"version": "1.0.0",
"description": "Export outstanding tasks from Obsidian vault to CSV for ManicTime integration",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
"version": "node version-bump.mjs && git add manifest.json versions.json"
},
"keywords": [
"obsidian",
"plugin",
"tasks",
"export",
"csv",
"time-tracking"
],
"author": "Raoul Jacobs",
"license": "MIT",
"devDependencies": {
"@types/node": "^16.11.6",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
"builtin-modules": "3.3.0",
"esbuild": "0.17.3",
"obsidian": "latest",
"tslib": "2.4.0",
"typescript": "4.7.4"
}
}

119
src/csv-writer.ts Normal file
View file

@ -0,0 +1,119 @@
import { TaskItem } from './types';
/**
* Exports tasks to CSV format with proper escaping.
* Ported from C# CsvExporter.cs
*/
export class CsvWriter {
/**
* Generates CSV content from tasks with UTF-8 BOM encoding.
*/
generateCsv(tasks: TaskItem[], compressLevels: boolean = false, includeHeader: boolean = true): string {
if (tasks.length === 0) {
throw new Error('No tasks to export');
}
// Determine the maximum level depth based on compression mode
const maxLevelDepth = compressLevels
? this.determineMaxCompressedLevelDepth(tasks)
: this.determineMaxLevelDepth(tasks);
let csv = '';
// Build header row based on max level depth
if (includeHeader) {
csv += 'CustomerName,ProjectName';
for (let i = 1; i <= maxLevelDepth; i++) {
csv += `,Level${i}`;
}
csv += ',Task\n';
}
// Write data rows
for (const task of tasks) {
csv += this.escapeField(task.customerName);
csv += ',';
csv += this.escapeField(task.projectName);
if (compressLevels) {
// Compressed mode: only output non-empty levels, skipping empty slots
const nonEmptyLevels = task.levels.filter(l => l !== '');
// Output non-empty levels (no padding needed - each row can have different column count)
for (const level of nonEmptyLevels) {
csv += ',';
csv += this.escapeField(level);
}
} else {
// Non-compressed mode: output all levels including empty slots to preserve hierarchy
for (let i = 0; i < maxLevelDepth; i++) {
csv += ',';
if (i < task.levels.length) {
csv += this.escapeField(task.levels[i]);
}
}
}
csv += ',';
csv += this.escapeField(task.task);
csv += '\n';
}
// Add UTF-8 BOM for Excel compatibility
return '\uFEFF' + csv;
}
/**
* Determines the maximum level depth across all tasks (includes empty slots).
*/
private determineMaxLevelDepth(tasks: TaskItem[]): number {
let maxDepth = 0;
for (const task of tasks) {
if (task.levels.length > maxDepth) {
maxDepth = task.levels.length;
}
}
return maxDepth;
}
/**
* Determines the maximum number of non-empty levels across all tasks.
*/
private determineMaxCompressedLevelDepth(tasks: TaskItem[]): number {
let maxDepth = 0;
for (const task of tasks) {
const nonEmptyCount = task.levels.filter(l => l !== '').length;
if (nonEmptyCount > maxDepth) {
maxDepth = nonEmptyCount;
}
}
return maxDepth;
}
/**
* Escapes a CSV field according to RFC 4180.
* - Wraps in quotes if contains comma, newline, or quote
* - Doubles any quotes inside the field
*/
private escapeField(field: string): string {
if (!field) {
return '';
}
// Check if escaping is needed
const needsQuotes = field.includes(',') || field.includes('"') || field.includes('\n') || field.includes('\r');
if (needsQuotes) {
// Double any existing quotes
field = field.replace(/"/g, '""');
// Wrap in quotes
return `"${field}"`;
}
return field;
}
}

111
src/exporter.ts Normal file
View file

@ -0,0 +1,111 @@
import { App, TFile, normalizePath } from 'obsidian';
import { TaskItem } from './types';
import { MarkdownParser } from './parser';
import { CsvWriter } from './csv-writer';
/**
* Extracts tasks from a directory structure of markdown files.
* Ported from C# TaskExtractor.cs
*/
export class TaskExporter {
private parser: MarkdownParser;
private csvWriter: CsvWriter;
private app: App;
constructor(app: App) {
this.app = app;
this.parser = new MarkdownParser();
this.csvWriter = new CsvWriter();
}
/**
* Extracts all outstanding tasks from the Customers directory.
*/
async extractTasks(customersFolder: string, verbose: boolean = false): Promise<TaskItem[]> {
const allTasks: TaskItem[] = [];
const normalizedPath = normalizePath(customersFolder);
// Check if folder exists
const folder = this.app.vault.getAbstractFileByPath(normalizedPath);
if (!folder) {
console.error(`Customers directory not found: ${normalizedPath}`);
return allTasks;
}
// Get all markdown files in the vault
const allFiles = this.app.vault.getMarkdownFiles();
// Filter files that are in the customers folder
const customerFiles = allFiles.filter(file =>
file.path.startsWith(normalizedPath + '/')
);
// Process each file
for (const file of customerFiles) {
// Extract customer name and project name from file path
const { customerName, projectName } = this.extractCustomerAndProject(file, normalizedPath);
// Skip files in hidden directories
if (customerName.startsWith('.')) {
continue;
}
if (verbose) {
console.log(`Processing: ${customerName} / ${projectName}`);
}
try {
const content = await this.app.vault.read(file);
const tasks = this.parser.parseFile(content, customerName, projectName);
if (tasks.length > 0) {
allTasks.push(...tasks);
if (verbose) {
console.log(` Found ${tasks.length} task(s)`);
}
}
} catch (ex) {
console.warn(`Failed to process file ${file.path}:`, ex);
}
}
return allTasks;
}
/**
* Exports tasks to CSV file.
*/
async exportToFile(
tasks: TaskItem[],
outputPath: string,
compressLevels: boolean = false,
includeHeader: boolean = true
): Promise<void> {
const csv = this.csvWriter.generateCsv(tasks, compressLevels, includeHeader);
const normalizedPath = normalizePath(outputPath);
await this.app.vault.adapter.write(normalizedPath, csv);
}
/**
* Extracts customer name and project name from file path.
* Customer name is the first folder under customersFolder.
* Project name is the filename without extension.
*/
private extractCustomerAndProject(file: TFile, customersFolder: string): { customerName: string, projectName: string } {
// Remove the customers folder prefix from the path
let relativePath = file.path.substring(customersFolder.length + 1);
// Split into parts
const parts = relativePath.split('/');
// Customer name is the first folder
const customerName = parts[0];
// Project name is the filename without extension
const projectName = file.basename;
return { customerName, projectName };
}
}

98
src/file-watcher.ts Normal file
View file

@ -0,0 +1,98 @@
import { TFile } from 'obsidian';
/**
* Manages file watching with debouncing for automatic export.
*/
export class FileWatcher {
private debounceTimer: number | null = null;
private callback: () => void;
private debounceDelay: number;
private isEnabled: boolean = false;
private customersFolder: string;
constructor(callback: () => void, debounceDelay: number, customersFolder: string) {
this.callback = callback;
this.debounceDelay = debounceDelay * 1000; // Convert to milliseconds
this.customersFolder = customersFolder;
}
/**
* Updates the debounce delay.
*/
setDebounceDelay(seconds: number): void {
this.debounceDelay = seconds * 1000;
}
/**
* Updates the customers folder path.
*/
setCustomersFolder(folder: string): void {
this.customersFolder = folder;
}
/**
* Triggers the debounced callback.
*/
trigger(): void {
if (!this.isEnabled) {
return;
}
// Clear existing timer
if (this.debounceTimer !== null) {
window.clearTimeout(this.debounceTimer);
}
// Set new timer
this.debounceTimer = window.setTimeout(() => {
this.callback();
this.debounceTimer = null;
}, this.debounceDelay);
}
/**
* Checks if a file should trigger an export.
*/
shouldTrigger(file: TFile | null): boolean {
if (!this.isEnabled || !file) {
return false;
}
// Only trigger for markdown files
if (file.extension !== 'md') {
return false;
}
// Only trigger for files in the customers folder
if (!file.path.startsWith(this.customersFolder + '/')) {
return false;
}
return true;
}
/**
* Enables the file watcher.
*/
enable(): void {
this.isEnabled = true;
}
/**
* Disables the file watcher and cancels any pending callbacks.
*/
disable(): void {
this.isEnabled = false;
if (this.debounceTimer !== null) {
window.clearTimeout(this.debounceTimer);
this.debounceTimer = null;
}
}
/**
* Returns whether the watcher is enabled.
*/
isWatcherEnabled(): boolean {
return this.isEnabled;
}
}

226
src/main.ts Normal file
View file

@ -0,0 +1,226 @@
import { App, Notice, Plugin, TFile } from 'obsidian';
import { TaskExportSettings, DEFAULT_SETTINGS } from './types';
import { TaskExportSettingTab } from './settings';
import { TaskExporter } from './exporter';
import { FileWatcher } from './file-watcher';
/**
* Main plugin class for Task Export Tool.
*/
export default class TaskExportPlugin extends Plugin {
settings: TaskExportSettings;
exporter: TaskExporter;
fileWatcher: FileWatcher;
statusBarItem: HTMLElement;
lastExportTime: Date | null = null;
exportInProgress: boolean = false;
async onload() {
await this.loadSettings();
// Initialize exporter
this.exporter = new TaskExporter(this.app);
// Initialize file watcher
this.fileWatcher = new FileWatcher(
() => this.autoExport(),
this.settings.debounceDelay,
this.settings.customersFolder
);
// Add ribbon icon
this.addRibbonIcon('file-text', 'Export Outstanding Tasks', async () => {
await this.exportTasks();
});
// Add command: Export Outstanding Tasks
this.addCommand({
id: 'export-tasks',
name: 'Export Outstanding Tasks',
callback: async () => {
await this.exportTasks();
}
});
// Add command: Toggle Auto-Export
this.addCommand({
id: 'toggle-auto-export',
name: 'Toggle Auto-Export',
callback: async () => {
this.settings.autoExport = !this.settings.autoExport;
await this.saveSettings();
this.updateFileWatcher();
const status = this.settings.autoExport ? 'enabled' : 'disabled';
new Notice(`Auto-export ${status}`);
}
});
// Add command: Open Export Settings
this.addCommand({
id: 'open-settings',
name: 'Open Export Settings',
callback: () => {
// @ts-ignore - accessing private API
this.app.setting.open();
// @ts-ignore - accessing private API
this.app.setting.openTabById(this.manifest.id);
}
});
// Add settings tab
this.addSettingTab(new TaskExportSettingTab(this.app, this));
// Add status bar item
this.statusBarItem = this.addStatusBarItem();
this.updateStatusBar();
// Register file events
this.registerFileEvents();
// Update file watcher state
this.updateFileWatcher();
console.log('Task Export Plugin loaded');
}
onunload() {
// Disable file watcher
this.fileWatcher.disable();
console.log('Task Export Plugin unloaded');
}
async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
}
async saveSettings() {
await this.saveData(this.settings);
}
/**
* Registers file modification events.
*/
private registerFileEvents() {
// Register vault modify event (fires when files are modified)
this.registerEvent(
this.app.vault.on('modify', (file) => {
if (file instanceof TFile && this.fileWatcher.shouldTrigger(file)) {
if (this.settings.exportOnModify || this.settings.exportOnSave) {
this.fileWatcher.trigger();
}
}
})
);
}
/**
* Updates the file watcher based on current settings.
*/
updateFileWatcher() {
if (this.settings.autoExport) {
this.fileWatcher.enable();
this.fileWatcher.setDebounceDelay(this.settings.debounceDelay);
this.fileWatcher.setCustomersFolder(this.settings.customersFolder);
} else {
this.fileWatcher.disable();
}
}
/**
* Performs automatic export (called by file watcher).
*/
private async autoExport() {
try {
await this.exportTasks(false); // Silent mode for auto-export
} catch (error) {
console.error('Auto-export failed:', error);
}
}
/**
* Exports tasks to CSV.
*/
async exportTasks(showNotice: boolean = true) {
// Prevent concurrent exports
if (this.exportInProgress) {
if (showNotice) {
new Notice('Export already in progress...');
}
return;
}
this.exportInProgress = true;
try {
// Extract tasks
const tasks = await this.exporter.extractTasks(this.settings.customersFolder, false);
if (tasks.length === 0) {
if (showNotice && this.settings.showNotifications) {
new Notice('No outstanding tasks found');
}
return;
}
// Export to CSV
await this.exporter.exportToFile(
tasks,
this.settings.outputPath,
this.settings.compressLevels,
this.settings.includeHeader
);
// Update last export time
this.lastExportTime = new Date();
this.updateStatusBar();
// Show success notification
if (showNotice && this.settings.showNotifications) {
new Notice(`Exported ${tasks.length} task(s) to ${this.settings.outputPath}`);
}
} catch (error) {
console.error('Export failed:', error);
if (showNotice && this.settings.showNotifications) {
new Notice(`Export failed: ${error.message}`);
}
} finally {
this.exportInProgress = false;
}
}
/**
* Updates the status bar with last export time.
*/
private updateStatusBar() {
if (!this.lastExportTime) {
this.statusBarItem.setText('');
return;
}
const now = new Date();
const diff = now.getTime() - this.lastExportTime.getTime();
const minutes = Math.floor(diff / 60000);
let timeText = '';
if (minutes < 1) {
timeText = 'just now';
} else if (minutes === 1) {
timeText = '1 minute ago';
} else if (minutes < 60) {
timeText = `${minutes} minutes ago`;
} else {
const hours = Math.floor(minutes / 60);
if (hours === 1) {
timeText = '1 hour ago';
} else {
timeText = `${hours} hours ago`;
}
}
this.statusBarItem.setText(`✅ Last export: ${timeText}`);
// Update again in a minute
window.setTimeout(() => this.updateStatusBar(), 60000);
}
}

153
src/parser.ts Normal file
View file

@ -0,0 +1,153 @@
import { TaskItem } from './types';
/**
* Parses markdown files to extract outstanding tasks with their hierarchical context.
* Ported from C# MarkdownParser.cs
*/
export class MarkdownParser {
private static readonly HEADER_REGEX = /^(#{2,})\s+(.+)$/;
private static readonly TASK_REGEX = /^(\s*)-\s+\[([ ])\]\s+(.+)$/;
private static readonly COMPLETED_TASK_REGEX = /^(\s*)-\s+\[(x|X)\]/;
private static readonly CHECKMARK_REGEX = /✅\s+\d{4}-\d{2}-\d{2}/;
/**
* Parses a markdown file and extracts all outstanding tasks.
*/
parseFile(content: string, customerName: string, projectName: string): TaskItem[] {
const tasks: TaskItem[] = [];
const lines = content.split('\n');
// Use an array to track headers at any depth (0-indexed, where 0 is ##, 1 is ###, etc.)
const headers: string[] = [];
// Track parent tasks for nested structure
const parentTaskStack: Array<{indentLevel: number, taskText: string}> = [];
for (let i = 0; i < lines.length; i++) {
const line = lines[i];
// Check for headers (##, ###, ####, etc.)
const headerMatch = line.match(MarkdownParser.HEADER_REGEX);
if (headerMatch) {
const headerLevel = headerMatch[1].length - 1; // Subtract 1 because ## is level 1 (index 0)
const headerText = headerMatch[2].trim();
// Ensure the headers array is large enough
while (headers.length <= headerLevel) {
headers.push('');
}
// Set this header level
headers[headerLevel] = headerText;
// Clear any deeper levels
for (let j = headerLevel + 1; j < headers.length; j++) {
headers[j] = '';
}
// Clear parent task stack when we hit a new header
parentTaskStack.length = 0;
continue;
}
// Skip completed tasks
if (MarkdownParser.COMPLETED_TASK_REGEX.test(line)) {
continue;
}
// Check for unchecked tasks
const taskMatch = line.match(MarkdownParser.TASK_REGEX);
if (taskMatch) {
const indent = taskMatch[1].length;
const taskText = taskMatch[3].trim();
// Skip tasks with checkmark indicators
if (MarkdownParser.CHECKMARK_REGEX.test(taskText)) {
continue;
}
// Check if this task has sub-tasks by looking ahead
const hasSubTasks = this.hasSubTasks(lines, i + 1, indent);
if (hasSubTasks) {
// This is a parent task, push it onto the stack
// Remove any parent tasks at the same or deeper level
while (parentTaskStack.length > 0 && parentTaskStack[parentTaskStack.length - 1].indentLevel >= indent) {
parentTaskStack.pop();
}
parentTaskStack.push({indentLevel: indent, taskText: taskText});
} else {
// This is a task to export
// Remove parent tasks at the same or deeper level
while (parentTaskStack.length > 0 && parentTaskStack[parentTaskStack.length - 1].indentLevel >= indent) {
parentTaskStack.pop();
}
// Build the task item with proper header hierarchy
const task: TaskItem = {
customerName: customerName,
projectName: projectName,
task: taskText,
levels: []
};
// Add document headers - keep all levels including empty ones for proper structure
const levels: string[] = [];
// Copy all header levels (including empty ones) to preserve hierarchy
levels.push(...headers);
// Add parent task levels
if (parentTaskStack.length > 0) {
const parents = [...parentTaskStack].reverse();
for (const parent of parents) {
levels.push(parent.taskText);
}
}
task.levels = levels;
tasks.push(task);
}
}
}
return tasks;
}
/**
* Checks if a task has sub-tasks by looking at following lines.
*/
private hasSubTasks(lines: string[], startIndex: number, currentIndent: number): boolean {
for (let i = startIndex; i < lines.length; i++) {
const line = lines[i];
// Empty lines don't matter
if (!line.trim()) {
continue;
}
// If we hit a header, no more sub-tasks
if (MarkdownParser.HEADER_REGEX.test(line)) {
return false;
}
// Check if this is a task
const taskMatch = line.match(MarkdownParser.TASK_REGEX);
if (taskMatch) {
const indent = taskMatch[1].length;
// If indented more than current task, it's a sub-task
if (indent > currentIndent) {
return true;
}
// If at same or less indent, no sub-tasks
if (indent <= currentIndent) {
return false;
}
}
}
return false;
}
}

128
src/settings.ts Normal file
View file

@ -0,0 +1,128 @@
import { App, PluginSettingTab, Setting } from 'obsidian';
import TaskExportPlugin from './main';
/**
* Settings tab for the Task Export plugin.
*/
export class TaskExportSettingTab extends PluginSettingTab {
plugin: TaskExportPlugin;
constructor(app: App, plugin: TaskExportPlugin) {
super(app, plugin);
this.plugin = plugin;
}
display(): void {
const { containerEl } = this;
containerEl.empty();
containerEl.createEl('h2', { text: 'Task Export Tool Settings' });
// Output Path
new Setting(containerEl)
.setName('Output path')
.setDesc('CSV file location (relative to vault root)')
.addText(text => text
.setPlaceholder('outstanding_tasks.csv')
.setValue(this.plugin.settings.outputPath)
.onChange(async (value) => {
this.plugin.settings.outputPath = value || 'outstanding_tasks.csv';
await this.plugin.saveSettings();
}));
// Customers Folder
new Setting(containerEl)
.setName('Customers folder')
.setDesc('Root folder containing customer files')
.addText(text => text
.setPlaceholder('Customers')
.setValue(this.plugin.settings.customersFolder)
.onChange(async (value) => {
this.plugin.settings.customersFolder = value || 'Customers';
await this.plugin.saveSettings();
this.plugin.updateFileWatcher();
}));
// Auto Export Toggle
new Setting(containerEl)
.setName('Auto export')
.setDesc('Automatically export tasks when files change')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.autoExport)
.onChange(async (value) => {
this.plugin.settings.autoExport = value;
await this.plugin.saveSettings();
this.plugin.updateFileWatcher();
}));
// Export on Save
new Setting(containerEl)
.setName('Export on save')
.setDesc('Trigger export when files are saved')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.exportOnSave)
.onChange(async (value) => {
this.plugin.settings.exportOnSave = value;
await this.plugin.saveSettings();
}));
// Export on Modify
new Setting(containerEl)
.setName('Export on modify')
.setDesc('Trigger export when files are modified (more frequent)')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.exportOnModify)
.onChange(async (value) => {
this.plugin.settings.exportOnModify = value;
await this.plugin.saveSettings();
}));
// Show Notifications
new Setting(containerEl)
.setName('Show notifications')
.setDesc('Display notifications on export completion')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.showNotifications)
.onChange(async (value) => {
this.plugin.settings.showNotifications = value;
await this.plugin.saveSettings();
}));
// Compress Levels
new Setting(containerEl)
.setName('Compress levels')
.setDesc('Remove empty hierarchy columns from CSV output')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.compressLevels)
.onChange(async (value) => {
this.plugin.settings.compressLevels = value;
await this.plugin.saveSettings();
}));
// Include Header
new Setting(containerEl)
.setName('Include header')
.setDesc('Include CSV header row in output')
.addToggle(toggle => toggle
.setValue(this.plugin.settings.includeHeader)
.onChange(async (value) => {
this.plugin.settings.includeHeader = value;
await this.plugin.saveSettings();
}));
// Debounce Delay
new Setting(containerEl)
.setName('Debounce delay')
.setDesc('Wait time after changes before exporting (1-30 seconds)')
.addSlider(slider => slider
.setLimits(1, 30, 1)
.setValue(this.plugin.settings.debounceDelay)
.setDynamicTooltip()
.onChange(async (value) => {
this.plugin.settings.debounceDelay = value;
await this.plugin.saveSettings();
this.plugin.updateFileWatcher();
}));
}
}

39
src/types.ts Normal file
View file

@ -0,0 +1,39 @@
/**
* Settings for the Task Export plugin
*/
export interface TaskExportSettings {
outputPath: string; // CSV output path (relative to vault)
customersFolder: string; // Root customer folder path
autoExport: boolean; // Enable auto-export
exportOnSave: boolean; // Trigger on file save
exportOnModify: boolean; // Trigger on file modify
showNotifications: boolean; // Show export notifications
compressLevels: boolean; // Remove empty hierarchy columns
includeHeader: boolean; // Include CSV header row
debounceDelay: number; // Debounce delay in seconds (1-30)
}
/**
* Default settings values
*/
export const DEFAULT_SETTINGS: TaskExportSettings = {
outputPath: 'outstanding_tasks.csv',
customersFolder: 'Customers',
autoExport: false,
exportOnSave: true,
exportOnModify: false,
showNotifications: true,
compressLevels: false,
includeHeader: true,
debounceDelay: 3
};
/**
* Represents a task extracted from a markdown file with its hierarchical context.
*/
export interface TaskItem {
customerName: string;
projectName: string;
levels: string[]; // Hierarchical headers and parent tasks
task: string; // The actual task text
}

3
styles.css Normal file
View file

@ -0,0 +1,3 @@
/* Task Export Plugin Styles */
/* Add any custom styling for the plugin here if needed */

24
tsconfig.json Normal file
View file

@ -0,0 +1,24 @@
{
"compilerOptions": {
"baseUrl": ".",
"inlineSourceMap": true,
"inlineSources": true,
"module": "ESNext",
"target": "ES6",
"allowJs": true,
"noImplicitAny": true,
"moduleResolution": "node",
"importHelpers": true,
"isolatedModules": true,
"strictNullChecks": true,
"lib": [
"DOM",
"ES5",
"ES6",
"ES7"
]
},
"include": [
"src/**/*.ts"
]
}

14
version-bump.mjs Normal file
View file

@ -0,0 +1,14 @@
import { readFileSync, writeFileSync } from "fs";
const targetVersion = process.env.npm_package_version;
// read minAppVersion from manifest.json and bump version to target version
let manifest = JSON.parse(readFileSync("manifest.json", "utf8"));
const { minAppVersion } = manifest;
manifest.version = targetVersion;
writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t"));
// update versions.json with target version and minAppVersion from manifest.json
let versions = JSON.parse(readFileSync("versions.json", "utf8"));
versions[targetVersion] = minAppVersion;
writeFileSync("versions.json", JSON.stringify(versions, null, "\t"));

3
versions.json Normal file
View file

@ -0,0 +1,3 @@
{
"1.0.0": "1.0.0"
}