mirror of
https://github.com/tailormade-eu/obsidian-task-export-plugin.git
synced 2026-07-22 16:30:23 +00:00
Compare commits
8 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc0c3f4f0 | ||
|
|
6fdae87c5a | ||
|
|
5c98896917 | ||
|
|
01a5d99d0b | ||
|
|
ea5ea0f4a1 | ||
|
|
97db6c49ca | ||
|
|
3ae738b61a | ||
|
|
12f39a7bba |
12 changed files with 101 additions and 51 deletions
21
CHANGELOG.md
Normal file
21
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
|
||||||
|
## [1.0.5] - 2025-11-21
|
||||||
|
### Added
|
||||||
|
- Include folder subfolders as hierarchical `Level1..LevelN` CSV columns. Folder segments under the configured `Customers` folder are now emitted as Level columns before the project filename and markdown headers.
|
||||||
|
- README examples updated to use generic placeholders (`<CustomerName>`, `<Subfolder>`, `<ProjectName>`, `<Task>`).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- CSV header format changed to: `CustomerName,Level1,Level2,...,Task` (project filename moved into `Level` columns to maintain consistent hierarchical ordering).
|
||||||
|
- CSV writer now uses a canonical `levels` array on parsed tasks which contains: folder segments, project filename, document headers, and parent task text.
|
||||||
|
|
||||||
|
### Notes
|
||||||
|
- The plugin bundle (`main.js`) should be rebuilt after the version bump. Run `npm run build` locally before publishing.
|
||||||
|
- If you distribute the plugin via GitHub Releases, create a release tagged `v1.0.5` and attach the rebuilt `main.js`, `manifest.json`, and `styles.css`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Previous releases
|
||||||
|
- See repository history for older versions.
|
||||||
16
README.md
16
README.md
|
|
@ -169,14 +169,26 @@ Standard Markdown task checkboxes:
|
||||||
CSV with dynamic columns (comma-separated by default, configurable to semicolon):
|
CSV with dynamic columns (comma-separated by default, configurable to semicolon):
|
||||||
|
|
||||||
```csv
|
```csv
|
||||||
CustomerName,ProjectName,Header1,Header2,Header3,Task
|
CustomerName,Level1,Level2,Level3,Task
|
||||||
Customer A,Project 1,Section,Outstanding task
|
Customer A,Project 1,Section,Outstanding task
|
||||||
Customer A,Project 1,Section,Task with sub-items
|
Customer A,Project 1,Section,Task with sub-items
|
||||||
Customer A,Project 1,Section,Sub-task (nested)
|
Customer A,Project 1,Section,Sub-task (nested)
|
||||||
Customer A,Project 1,Section,Subsection,Another task
|
Customer A,Project 1,Section,Subsection,Another task
|
||||||
```
|
```
|
||||||
|
|
||||||
The delimiter can be changed to semicolon (`;`) in the plugin settings for better compatibility with European Excel versions.
|
Notes:
|
||||||
|
- **Level columns**: The plugin uses generic `Level1..LevelN` columns for hierarchical path data. These contain, in order: any subfolder segments under the customer folder, then the project (markdown file) name, followed by markdown header segments and parent task text as present.
|
||||||
|
- **Compression**: When `Compress Levels` is enabled the CSV will omit empty trailing `Level` columns for each row; when disabled rows are padded to the maximum level depth.
|
||||||
|
- The delimiter can be changed to semicolon (`;`) in the plugin settings for better compatibility with European Excel versions.
|
||||||
|
|
||||||
|
Example
|
||||||
|
|
||||||
|
For a file under the `Customers` folder such as `Customers/<CustomerName>/<Subfolder...>/<ProjectName>.md` containing a top-level task in a `Section`, a produced CSV row will look like:
|
||||||
|
|
||||||
|
```csv
|
||||||
|
CustomerName,Level1,Level2,Task
|
||||||
|
<CustomerName>,<Subfolder>,<ProjectName>,Section - Outstanding task
|
||||||
|
```
|
||||||
|
|
||||||
## Technical Details
|
## Technical Details
|
||||||
|
|
||||||
|
|
|
||||||
36
main.js
36
main.js
|
|
@ -53,9 +53,8 @@ var TaskExportSettingTab = class extends import_obsidian.PluginSettingTab {
|
||||||
display() {
|
display() {
|
||||||
const { containerEl } = this;
|
const { containerEl } = this;
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
new import_obsidian.Setting(containerEl).setName("Task Export Tool Settings").setHeading();
|
new import_obsidian.Setting(containerEl).setName("Output path").setDesc("CSV file location (relative to vault root)").addText((text) => text.setPlaceholder("tasks.csv").setValue(this.plugin.settings.outputPath).onChange((value) => {
|
||||||
new import_obsidian.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((value) => {
|
this.plugin.settings.outputPath = value || "tasks.csv";
|
||||||
this.plugin.settings.outputPath = value || "outstanding_tasks.csv";
|
|
||||||
void this.plugin.saveSettings();
|
void this.plugin.saveSettings();
|
||||||
}));
|
}));
|
||||||
new import_obsidian.Setting(containerEl).setName("Customers folder").setDesc("Root folder containing customer files").addText((text) => text.setPlaceholder("Customers").setValue(this.plugin.settings.customersFolder).onChange((value) => {
|
new import_obsidian.Setting(containerEl).setName("Customers folder").setDesc("Root folder containing customer files").addText((text) => text.setPlaceholder("Customers").setValue(this.plugin.settings.customersFolder).onChange((value) => {
|
||||||
|
|
@ -88,7 +87,7 @@ var TaskExportSettingTab = class extends import_obsidian.PluginSettingTab {
|
||||||
this.plugin.settings.includeHeader = value;
|
this.plugin.settings.includeHeader = value;
|
||||||
void this.plugin.saveSettings();
|
void this.plugin.saveSettings();
|
||||||
}));
|
}));
|
||||||
new import_obsidian.Setting(containerEl).setName("CSV delimiter").setDesc("Choose delimiter for CSV output. Comma is standard, semicolon is common in Europe.").addDropdown((dropdown) => dropdown.addOption(",", "Comma (,)").addOption(";", "Semicolon (;)").setValue(this.plugin.settings.delimiter).onChange((value) => {
|
new import_obsidian.Setting(containerEl).setName("CSV delimiter").setDesc("Delimiter for CSV output: comma is standard, semicolon is common in Europe").addDropdown((dropdown) => dropdown.addOption(",", "Comma (,)").addOption(";", "Semicolon (;)").setValue(this.plugin.settings.delimiter).onChange((value) => {
|
||||||
this.plugin.settings.delimiter = value;
|
this.plugin.settings.delimiter = value;
|
||||||
void this.plugin.saveSettings();
|
void this.plugin.saveSettings();
|
||||||
}));
|
}));
|
||||||
|
|
@ -108,7 +107,7 @@ var _MarkdownParser = class {
|
||||||
/**
|
/**
|
||||||
* Parses a markdown file and extracts all outstanding tasks.
|
* Parses a markdown file and extracts all outstanding tasks.
|
||||||
*/
|
*/
|
||||||
parseFile(content, customerName, projectName) {
|
parseFile(content, customerName, projectName, folderSegments = []) {
|
||||||
const tasks = [];
|
const tasks = [];
|
||||||
const lines = content.split("\n");
|
const lines = content.split("\n");
|
||||||
const headers = [];
|
const headers = [];
|
||||||
|
|
@ -156,6 +155,12 @@ var _MarkdownParser = class {
|
||||||
levels: []
|
levels: []
|
||||||
};
|
};
|
||||||
const levels = [];
|
const levels = [];
|
||||||
|
if (folderSegments && folderSegments.length > 0) {
|
||||||
|
levels.push(...folderSegments);
|
||||||
|
}
|
||||||
|
if (projectName && projectName.length > 0) {
|
||||||
|
levels.push(projectName);
|
||||||
|
}
|
||||||
levels.push(...headers);
|
levels.push(...headers);
|
||||||
if (parentTaskStack.length > 0) {
|
if (parentTaskStack.length > 0) {
|
||||||
const parents = [...parentTaskStack].reverse();
|
const parents = [...parentTaskStack].reverse();
|
||||||
|
|
@ -217,7 +222,7 @@ var CsvWriter = class {
|
||||||
const maxLevelDepth = compressLevels ? this.determineMaxCompressedLevelDepth(tasks) : this.determineMaxLevelDepth(tasks);
|
const maxLevelDepth = compressLevels ? this.determineMaxCompressedLevelDepth(tasks) : this.determineMaxLevelDepth(tasks);
|
||||||
let csv = "";
|
let csv = "";
|
||||||
if (includeHeader) {
|
if (includeHeader) {
|
||||||
csv += `CustomerName${this.delimiter}ProjectName`;
|
csv += `CustomerName`;
|
||||||
for (let i = 1; i <= maxLevelDepth; i++) {
|
for (let i = 1; i <= maxLevelDepth; i++) {
|
||||||
csv += `${this.delimiter}Level${i}`;
|
csv += `${this.delimiter}Level${i}`;
|
||||||
}
|
}
|
||||||
|
|
@ -226,8 +231,6 @@ var CsvWriter = class {
|
||||||
}
|
}
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
csv += this.escapeField(task.customerName);
|
csv += this.escapeField(task.customerName);
|
||||||
csv += this.delimiter;
|
|
||||||
csv += this.escapeField(task.projectName);
|
|
||||||
if (compressLevels) {
|
if (compressLevels) {
|
||||||
const nonEmptyLevels = task.levels.filter((l) => l !== "");
|
const nonEmptyLevels = task.levels.filter((l) => l !== "");
|
||||||
for (const level of nonEmptyLevels) {
|
for (const level of nonEmptyLevels) {
|
||||||
|
|
@ -314,7 +317,7 @@ var TaskExporter = class {
|
||||||
(file) => file.path.startsWith(normalizedPath + "/")
|
(file) => file.path.startsWith(normalizedPath + "/")
|
||||||
);
|
);
|
||||||
for (const file of customerFiles) {
|
for (const file of customerFiles) {
|
||||||
const { customerName, projectName } = this.extractCustomerAndProject(file, normalizedPath);
|
const { customerName, projectName, folderSegments } = this.extractCustomerAndProject(file, normalizedPath);
|
||||||
if (customerName.startsWith(".")) {
|
if (customerName.startsWith(".")) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
@ -323,7 +326,7 @@ var TaskExporter = class {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const content = await this.app.vault.read(file);
|
const content = await this.app.vault.read(file);
|
||||||
const tasks = this.parser.parseFile(content, customerName, projectName);
|
const tasks = this.parser.parseFile(content, customerName, projectName, folderSegments);
|
||||||
if (tasks.length > 0) {
|
if (tasks.length > 0) {
|
||||||
allTasks.push(...tasks);
|
allTasks.push(...tasks);
|
||||||
if (verbose) {
|
if (verbose) {
|
||||||
|
|
@ -355,7 +358,8 @@ var TaskExporter = class {
|
||||||
const parts = relativePath.split("/");
|
const parts = relativePath.split("/");
|
||||||
const customerName = parts[0];
|
const customerName = parts[0];
|
||||||
const projectName = file.basename;
|
const projectName = file.basename;
|
||||||
return { customerName, projectName };
|
const folderSegments = parts.length > 2 ? parts.slice(1, parts.length - 1) : [];
|
||||||
|
return { customerName, projectName, folderSegments };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -445,23 +449,23 @@ var TaskExportPlugin = class extends import_obsidian3.Plugin {
|
||||||
await this.loadSettings();
|
await this.loadSettings();
|
||||||
this.exporter = new TaskExporter(this.app);
|
this.exporter = new TaskExporter(this.app);
|
||||||
this.fileWatcher = new FileWatcher(
|
this.fileWatcher = new FileWatcher(
|
||||||
() => this.autoExport(),
|
() => void this.autoExport(),
|
||||||
this.settings.debounceDelay,
|
this.settings.debounceDelay,
|
||||||
this.settings.customersFolder
|
this.settings.customersFolder
|
||||||
);
|
);
|
||||||
this.addRibbonIcon("file-text", "Export Outstanding Tasks", async () => {
|
this.addRibbonIcon("file-text", "Export outstanding tasks", async () => {
|
||||||
await this.exportTasks();
|
await this.exportTasks();
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "export-tasks",
|
id: "export-tasks",
|
||||||
name: "Export Outstanding Tasks",
|
name: "Export outstanding tasks",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await this.exportTasks();
|
await this.exportTasks();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "toggle-auto-export",
|
id: "toggle-auto-export",
|
||||||
name: "Toggle Auto-Export",
|
name: "Toggle auto-export",
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
this.settings.autoExport = !this.settings.autoExport;
|
this.settings.autoExport = !this.settings.autoExport;
|
||||||
await this.saveSettings();
|
await this.saveSettings();
|
||||||
|
|
@ -472,7 +476,7 @@ var TaskExportPlugin = class extends import_obsidian3.Plugin {
|
||||||
});
|
});
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "open-settings",
|
id: "open-settings",
|
||||||
name: "Open Export Settings",
|
name: "Open export settings",
|
||||||
callback: () => {
|
callback: () => {
|
||||||
this.app.setting.open();
|
this.app.setting.open();
|
||||||
this.app.setting.openTabById(this.manifest.id);
|
this.app.setting.openTabById(this.manifest.id);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"id": "task-export-to-csv",
|
"id": "task-export-to-csv",
|
||||||
"name": "Task Export Tool",
|
"name": "Task Export Tool",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"minAppVersion": "1.0.0",
|
"minAppVersion": "1.0.0",
|
||||||
"description": "Export outstanding tasks to CSV for time tracking integration (e.g. ManicTime).",
|
"description": "Export outstanding tasks to CSV for time tracking integration (e.g. ManicTime).",
|
||||||
"author": "Raoul Jacobs",
|
"author": "Raoul Jacobs",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "obsidian-task-export-plugin",
|
"name": "obsidian-task-export-plugin",
|
||||||
"version": "1.0.2",
|
"version": "1.0.5",
|
||||||
"description": "Export outstanding tasks from Obsidian vault to CSV for ManicTime integration",
|
"description": "Export outstanding tasks from Obsidian vault to CSV for ManicTime integration",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,8 @@ export class CsvWriter {
|
||||||
|
|
||||||
// Build header row based on max level depth
|
// Build header row based on max level depth
|
||||||
if (includeHeader) {
|
if (includeHeader) {
|
||||||
csv += `CustomerName${this.delimiter}ProjectName`;
|
// Emit CustomerName, then Level columns (first level may be project name or a subfolder), then Task
|
||||||
|
csv += `CustomerName`;
|
||||||
for (let i = 1; i <= maxLevelDepth; i++) {
|
for (let i = 1; i <= maxLevelDepth; i++) {
|
||||||
csv += `${this.delimiter}Level${i}`;
|
csv += `${this.delimiter}Level${i}`;
|
||||||
}
|
}
|
||||||
|
|
@ -38,8 +39,6 @@ export class CsvWriter {
|
||||||
// Write data rows
|
// Write data rows
|
||||||
for (const task of tasks) {
|
for (const task of tasks) {
|
||||||
csv += this.escapeField(task.customerName);
|
csv += this.escapeField(task.customerName);
|
||||||
csv += this.delimiter;
|
|
||||||
csv += this.escapeField(task.projectName);
|
|
||||||
|
|
||||||
if (compressLevels) {
|
if (compressLevels) {
|
||||||
// Compressed mode: only output non-empty levels, skipping empty slots
|
// Compressed mode: only output non-empty levels, skipping empty slots
|
||||||
|
|
@ -59,7 +58,6 @@ export class CsvWriter {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
csv += this.delimiter;
|
csv += this.delimiter;
|
||||||
csv += this.escapeField(task.task);
|
csv += this.escapeField(task.task);
|
||||||
csv += '\n';
|
csv += '\n';
|
||||||
|
|
|
||||||
|
|
@ -43,8 +43,8 @@ export class TaskExporter {
|
||||||
|
|
||||||
// Process each file
|
// Process each file
|
||||||
for (const file of customerFiles) {
|
for (const file of customerFiles) {
|
||||||
// Extract customer name and project name from file path
|
// Extract customer name, project name and folder segments from file path
|
||||||
const { customerName, projectName } = this.extractCustomerAndProject(file, normalizedPath);
|
const { customerName, projectName, folderSegments } = this.extractCustomerAndProject(file, normalizedPath);
|
||||||
|
|
||||||
// Skip files in hidden directories
|
// Skip files in hidden directories
|
||||||
if (customerName.startsWith('.')) {
|
if (customerName.startsWith('.')) {
|
||||||
|
|
@ -57,7 +57,8 @@ export class TaskExporter {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const content = await this.app.vault.read(file);
|
const content = await this.app.vault.read(file);
|
||||||
const tasks = this.parser.parseFile(content, customerName, projectName);
|
// Pass folderSegments into parser so levels can include folder hierarchy
|
||||||
|
const tasks = this.parser.parseFile(content, customerName, projectName, folderSegments);
|
||||||
|
|
||||||
if (tasks.length > 0) {
|
if (tasks.length > 0) {
|
||||||
allTasks.push(...tasks);
|
allTasks.push(...tasks);
|
||||||
|
|
@ -96,7 +97,7 @@ export class TaskExporter {
|
||||||
* Customer name is the first folder under customersFolder.
|
* Customer name is the first folder under customersFolder.
|
||||||
* Project name is the filename without extension.
|
* Project name is the filename without extension.
|
||||||
*/
|
*/
|
||||||
private extractCustomerAndProject(file: TFile, customersFolder: string): { customerName: string, projectName: string } {
|
private extractCustomerAndProject(file: TFile, customersFolder: string): { customerName: string, projectName: string, folderSegments: string[] } {
|
||||||
// Remove the customers folder prefix from the path
|
// Remove the customers folder prefix from the path
|
||||||
let relativePath = file.path.substring(customersFolder.length + 1);
|
let relativePath = file.path.substring(customersFolder.length + 1);
|
||||||
|
|
||||||
|
|
@ -109,6 +110,10 @@ export class TaskExporter {
|
||||||
// Project name is the filename without extension
|
// Project name is the filename without extension
|
||||||
const projectName = file.basename;
|
const projectName = file.basename;
|
||||||
|
|
||||||
return { customerName, projectName };
|
// Folder segments are the intermediate path parts between the customer folder and the file name
|
||||||
|
// parts = [ customer, sub1, sub2, ..., filename ] so folderSegments should be parts.slice(1, parts.length - 1)
|
||||||
|
const folderSegments = parts.length > 2 ? parts.slice(1, parts.length - 1) : [];
|
||||||
|
|
||||||
|
return { customerName, projectName, folderSegments };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
src/main.ts
10
src/main.ts
|
|
@ -23,20 +23,20 @@ export default class TaskExportPlugin extends Plugin {
|
||||||
|
|
||||||
// Initialize file watcher
|
// Initialize file watcher
|
||||||
this.fileWatcher = new FileWatcher(
|
this.fileWatcher = new FileWatcher(
|
||||||
() => this.autoExport(),
|
() => void this.autoExport(),
|
||||||
this.settings.debounceDelay,
|
this.settings.debounceDelay,
|
||||||
this.settings.customersFolder
|
this.settings.customersFolder
|
||||||
);
|
);
|
||||||
|
|
||||||
// Add ribbon icon
|
// Add ribbon icon
|
||||||
this.addRibbonIcon('file-text', 'Export Outstanding Tasks', async () => {
|
this.addRibbonIcon('file-text', 'Export outstanding tasks', async () => {
|
||||||
await this.exportTasks();
|
await this.exportTasks();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add command: Export Outstanding Tasks
|
// Add command: Export Outstanding Tasks
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'export-tasks',
|
id: 'export-tasks',
|
||||||
name: 'Export Outstanding Tasks',
|
name: 'Export outstanding tasks',
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
await this.exportTasks();
|
await this.exportTasks();
|
||||||
}
|
}
|
||||||
|
|
@ -45,7 +45,7 @@ export default class TaskExportPlugin extends Plugin {
|
||||||
// Add command: Toggle Auto-Export
|
// Add command: Toggle Auto-Export
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'toggle-auto-export',
|
id: 'toggle-auto-export',
|
||||||
name: 'Toggle Auto-Export',
|
name: 'Toggle auto-export',
|
||||||
callback: async () => {
|
callback: async () => {
|
||||||
this.settings.autoExport = !this.settings.autoExport;
|
this.settings.autoExport = !this.settings.autoExport;
|
||||||
await this.saveSettings();
|
await this.saveSettings();
|
||||||
|
|
@ -59,7 +59,7 @@ export default class TaskExportPlugin extends Plugin {
|
||||||
// Add command: Open Export Settings
|
// Add command: Open Export Settings
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'open-settings',
|
id: 'open-settings',
|
||||||
name: 'Open Export Settings',
|
name: 'Open export settings',
|
||||||
callback: () => {
|
callback: () => {
|
||||||
// @ts-ignore - accessing private API
|
// @ts-ignore - accessing private API
|
||||||
this.app.setting.open();
|
this.app.setting.open();
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ export class MarkdownParser {
|
||||||
/**
|
/**
|
||||||
* Parses a markdown file and extracts all outstanding tasks.
|
* Parses a markdown file and extracts all outstanding tasks.
|
||||||
*/
|
*/
|
||||||
parseFile(content: string, customerName: string, projectName: string): TaskItem[] {
|
parseFile(content: string, customerName: string, projectName: string, folderSegments: string[] = []): TaskItem[] {
|
||||||
const tasks: TaskItem[] = [];
|
const tasks: TaskItem[] = [];
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
|
@ -91,9 +91,19 @@ export class MarkdownParser {
|
||||||
levels: []
|
levels: []
|
||||||
};
|
};
|
||||||
|
|
||||||
// Add document headers - keep all levels including empty ones for proper structure
|
// Build levels with folder segments first, then document headers
|
||||||
const levels: string[] = [];
|
const levels: string[] = [];
|
||||||
|
|
||||||
|
// Insert folder segments (subfolders under the customer folder)
|
||||||
|
if (folderSegments && folderSegments.length > 0) {
|
||||||
|
levels.push(...folderSegments);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Insert the project name as a level after folder segments (so it appears after subfolders)
|
||||||
|
if (projectName && projectName.length > 0) {
|
||||||
|
levels.push(projectName);
|
||||||
|
}
|
||||||
|
|
||||||
// Copy all header levels (including empty ones) to preserve hierarchy
|
// Copy all header levels (including empty ones) to preserve hierarchy
|
||||||
levels.push(...headers);
|
levels.push(...headers);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { PluginSettingTab, Setting } from 'obsidian';
|
import { App, PluginSettingTab, Setting } from 'obsidian';
|
||||||
import TaskExportPlugin from './main';
|
import TaskExportPlugin from './main';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -7,7 +7,7 @@ import TaskExportPlugin from './main';
|
||||||
export class TaskExportSettingTab extends PluginSettingTab {
|
export class TaskExportSettingTab extends PluginSettingTab {
|
||||||
plugin: TaskExportPlugin;
|
plugin: TaskExportPlugin;
|
||||||
|
|
||||||
constructor(app: any, plugin: TaskExportPlugin) {
|
constructor(app: App, plugin: TaskExportPlugin) {
|
||||||
super(app, plugin);
|
super(app, plugin);
|
||||||
this.plugin = plugin;
|
this.plugin = plugin;
|
||||||
}
|
}
|
||||||
|
|
@ -17,19 +17,15 @@ export class TaskExportSettingTab extends PluginSettingTab {
|
||||||
|
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
|
|
||||||
new Setting(containerEl)
|
|
||||||
.setName('Task Export Tool Settings')
|
|
||||||
.setHeading();
|
|
||||||
|
|
||||||
// Output Path
|
// Output Path
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('Output path')
|
.setName('Output path')
|
||||||
.setDesc('CSV file location (relative to vault root)')
|
.setDesc('CSV file location (relative to vault root)')
|
||||||
.addText(text => text
|
.addText(text => text
|
||||||
.setPlaceholder('outstanding_tasks.csv')
|
.setPlaceholder('tasks.csv')
|
||||||
.setValue(this.plugin.settings.outputPath)
|
.setValue(this.plugin.settings.outputPath)
|
||||||
.onChange((value) => {
|
.onChange((value) => {
|
||||||
this.plugin.settings.outputPath = value || 'outstanding_tasks.csv';
|
this.plugin.settings.outputPath = value || 'tasks.csv';
|
||||||
void this.plugin.saveSettings();
|
void this.plugin.saveSettings();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
@ -116,7 +112,7 @@ export class TaskExportSettingTab extends PluginSettingTab {
|
||||||
// CSV Delimiter
|
// CSV Delimiter
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName('CSV delimiter')
|
.setName('CSV delimiter')
|
||||||
.setDesc('Choose delimiter for CSV output. Comma is standard, semicolon is common in Europe.')
|
.setDesc('Delimiter for CSV output: comma is standard, semicolon is common in Europe')
|
||||||
.addDropdown(dropdown => dropdown
|
.addDropdown(dropdown => dropdown
|
||||||
.addOption(',', 'Comma (,)')
|
.addOption(',', 'Comma (,)')
|
||||||
.addOption(';', 'Semicolon (;)')
|
.addOption(';', 'Semicolon (;)')
|
||||||
|
|
|
||||||
|
|
@ -36,6 +36,8 @@ export const DEFAULT_SETTINGS: TaskExportSettings = {
|
||||||
export interface TaskItem {
|
export interface TaskItem {
|
||||||
customerName: string;
|
customerName: string;
|
||||||
projectName: string;
|
projectName: string;
|
||||||
levels: string[]; // Hierarchical headers and parent tasks
|
// `levels` contains folder segments (subfolders under the customer),
|
||||||
|
// followed by document header levels (##, ###, ...), then parent task texts.
|
||||||
|
levels: string[];
|
||||||
task: string; // The actual task text
|
task: string; // The actual task text
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"1.0.0": "1.0.0",
|
"1.0.0": "1.0.0",
|
||||||
"1.0.1": "1.0.0",
|
"1.0.1": "1.0.0",
|
||||||
"1.0.2": "1.0.0"
|
"1.0.2": "1.0.0",
|
||||||
|
"1.0.3": "1.0.0",
|
||||||
|
"1.0.4": "1.0.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue