From cfd3a6ef5d6a4485eb5d763ba0707b43dfd172dc Mon Sep 17 00:00:00 2001 From: Zsolt Viczian Date: Fri, 16 Apr 2021 07:23:26 +0200 Subject: [PATCH] added exclude filename and path --- data.json | 2 +- src/main.ts | 279 ++++++------ src/model/TodoIndex.ts | 257 +++++------ src/model/TodoParser.test.ts | 136 +++--- src/settings.ts | 673 +++++++++++++++-------------- src/ui/TodoItemView.ts | 814 +++++++++++++++++------------------ 6 files changed, 1105 insertions(+), 1056 deletions(-) diff --git a/data.json b/data.json index 7d89f34..7883ec5 100644 --- a/data.json +++ b/data.json @@ -1 +1 @@ -{"personRegexpString":"\\[{2}People\\/(.*?)\\]{2}","projectRegexpString":"\\[{2}Projects\\/(.*?)\\]{2}","miscRegexpString":"","dateRegexpString":"#(\\d{4})\\/(\\d{2})\\/(\\d{2})","discussWithRegexpString":"#(discussWith)","waitingForRegexpString":"#(waitingFor)","promisedToRegexpString":"#(promisedTo)","somedayMaybeRegexpString":"#(someday)","isInboxVisible":true,"isAgingVisible":true,"isTodayVisible":true,"isScheduledVisible":true,"isStakeholderVisible":true,"isSomedayVisible":true,"inboxTooltip":"Inbox: No date set, no stakeholder action set, not a someday / maybe item.","agingTooltip":"Aging...","todayTooltip":"Scheduled for Today","scheduledTooltip":"Scheduled for a future date","stakeholderTooltip":"Stakeholder and Project actions: discussWith, promisedTo, waitingFor. Only items that have a valid project or person will show up here. Stakeholder actions without project or person are in the Inbox.","somedayTooltip":"Tagged as Someday / Maybe"} \ No newline at end of file +{"personRegexpString":"\\[{2}People\\/(.*?)\\]{2}","projectRegexpString":"\\[{2}Projects\\/(.*?)\\]{2}","miscRegexpString":"","dateRegexpString":"#(\\d{4})\\/(\\d{2})\\/(\\d{2})","discussWithRegexpString":"#(discussWith)","waitingForRegexpString":"#(waitingFor)","promisedToRegexpString":"#(promisedTo)","somedayMaybeRegexpString":"#(someday)","excludePath":"Templates/","excludeFilenameFragment":"checklist","isInboxVisible":true,"isAgingVisible":true,"isTodayVisible":true,"isScheduledVisible":true,"isStakeholderVisible":true,"isSomedayVisible":true,"inboxTooltip":"Inbox: No date set, no stakeholder action set, not a someday / maybe item.","agingTooltip":"Aging...","todayTooltip":"Scheduled for Today","scheduledTooltip":"Scheduled for a future date","stakeholderTooltip":"Stakeholder and Project actions: discussWith, promisedTo, waitingFor. Only items that have a valid project or person will show up here. Stakeholder actions without project or person are in the Inbox.","somedayTooltip":"Tagged as Someday / Maybe"} \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 138a3ec..567b797 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,138 +1,141 @@ -import { App, Plugin, PluginManifest, TFile, WorkspaceLeaf, } from 'obsidian'; -import { VIEW_TYPE_TODO } from './constants'; -import { TodoItemView, TodoItemViewProps } from './ui/TodoItemView'; -import { TodoItem, TodoItemStatus } from './model/TodoItem'; -import { TodoIndex,TodoItemIndexProps } from './model/TodoIndex'; -import {DEFAULT_SETTINGS, ActionTrackerSettings, ActionTrackerSettingTab} from './settings'; - - -export default class ActionTrackerPlugin extends Plugin { - private todoIndex: TodoIndex; - private view: TodoItemView; - settings: ActionTrackerSettings; - - constructor(app: App, manifest: PluginManifest) { - super(app, manifest); - } - - private getTodoItemIndexProps() : TodoItemIndexProps { - return { - personRegexp: new RegExp (this.getSettingValue('personRegexpString')), - projectRegexp: new RegExp (this.getSettingValue('projectRegexpString')), - miscRegexp: new RegExp (this.getSettingValue('miscRegexpString')), - dateRegexp: new RegExp (this.getSettingValue('dateRegexpString')), - discussWithRegexp: new RegExp (this.getSettingValue('discussWithRegexpString')), - waitingForRegexp: new RegExp (this.getSettingValue('waitingForRegexpString')), - promisedToRegexp: new RegExp (this.getSettingValue('promisedToRegexpString')), - somedayMaybeRegexp: new RegExp (this.getSettingValue('somedayMaybeRegexpString')), - }; - } - - async onload(): Promise { - console.log('loading plugin'); - - await this.loadSettings(); - - this.todoIndex = new TodoIndex(this.app.vault, this.tick.bind(this),this.getTodoItemIndexProps()); - - this.registerView(VIEW_TYPE_TODO, (leaf: WorkspaceLeaf) => { - const todos: TodoItem[] = []; - const props = { - todos: todos, - openFile: (filePath: string) => { - const file = this.app.vault.getAbstractFileByPath(filePath) as TFile; - this.app.workspace.splitActiveLeaf().openFile(file); - }, - toggleTodo: (todo: TodoItem, newStatus: TodoItemStatus) => { - this.todoIndex.setStatus(todo, newStatus); - }, - isInboxVisible: this.getSettingValue('isInboxVisible'), - isAgingVisible: this.getSettingValue('isAgingVisible'), - isTodayVisible: this.getSettingValue('isTodayVisible'), - isScheduledVisible: this.getSettingValue('isScheduledVisible'), - isStakeholderVisible: this.getSettingValue('isStakeholderVisible'), - isSomedayVisible: this.getSettingValue('isSomedayVisible'), - inboxTooltip: this.getSettingValue('inboxTooltip'), - agingTooltip: this.getSettingValue('agingTooltip'), - todayTooltip: this.getSettingValue('todayTooltip'), - scheduledTooltip: this.getSettingValue('scheduledTooltip'), - stakeholderTooltip: this.getSettingValue('stakeholderTooltip'), - somedayTooltip: this.getSettingValue('somedayTooltip'), - }; - this.view = new TodoItemView(leaf, props); - return this.view; - }); - - this.addSettingTab(new ActionTrackerSettingTab(this.app, this)); - - if (this.app.workspace.layoutReady) { - this.initLeaf(); - await this.prepareIndex(); - } else { - this.registerEvent(this.app.workspace.on('layout-ready', this.initLeaf.bind(this))); - this.registerEvent(this.app.workspace.on('layout-ready', async () => await this.prepareIndex())); - } - } - - onunload(): void { - this.app.workspace.getLeavesOfType(VIEW_TYPE_TODO).forEach((leaf) => leaf.detach()); - } - - initLeaf(): void { - if (this.app.workspace.getLeavesOfType(VIEW_TYPE_TODO).length) { - return; - } - this.app.workspace.getRightLeaf(false).setViewState({ - type: VIEW_TYPE_TODO, - }); - } - - async prepareIndex(): Promise { - await this.todoIndex.initialize(); - } - - tick(todos: TodoItem[]): void { - this.view.setProps((currentProps: TodoItemViewProps) => { - return { - ...currentProps, - todos: todos, - }; - }); - } - - async loadSettings() { - this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); - } - - async saveFilterSettings() { - await this.saveData(this.settings); - await this.todoIndex.reloadIndex(this.getTodoItemIndexProps()); - } - - async saveViewDisplaySettings() { - await this.saveData(this.settings); - this.view.setDisplayProps({ - todos: null, - openFile: null, - toggleTodo: null, - isInboxVisible: this.getSettingValue('isInboxVisible'), - isAgingVisible: this.getSettingValue('isAgingVisible'), - isTodayVisible: this.getSettingValue('isTodayVisible'), - isScheduledVisible: this.getSettingValue('isScheduledVisible'), - isStakeholderVisible: this.getSettingValue('isStakeholderVisible'), - isSomedayVisible: this.getSettingValue('isSomedayVisible'), - inboxTooltip: this.getSettingValue('inboxTooltip'), - agingTooltip: this.getSettingValue('agingTooltip'), - todayTooltip: this.getSettingValue('todayTooltip'), - scheduledTooltip: this.getSettingValue('scheduledTooltip'), - stakeholderTooltip: this.getSettingValue('stakeholderTooltip'), - somedayTooltip: this.getSettingValue('somedayTooltip'), - }); - } - - getSettingValue(setting: K): ActionTrackerSettings[K] { - return this.settings[setting] - } -} - - +import { App, Plugin, PluginManifest, TFile, WorkspaceLeaf, } from 'obsidian'; +import { VIEW_TYPE_TODO } from './constants'; +import { TodoItemView, TodoItemViewProps } from './ui/TodoItemView'; +import { TodoItem, TodoItemStatus } from './model/TodoItem'; +import { TodoIndex,TodoItemIndexProps } from './model/TodoIndex'; +import {DEFAULT_SETTINGS, ActionTrackerSettings, ActionTrackerSettingTab} from './settings'; +import { stringify } from 'querystring'; + + +export default class ActionTrackerPlugin extends Plugin { + private todoIndex: TodoIndex; + private view: TodoItemView; + settings: ActionTrackerSettings; + + constructor(app: App, manifest: PluginManifest) { + super(app, manifest); + } + + private getTodoItemIndexProps() : TodoItemIndexProps { + return { + personRegexp: new RegExp (this.getSettingValue('personRegexpString')), + projectRegexp: new RegExp (this.getSettingValue('projectRegexpString')), + miscRegexp: new RegExp (this.getSettingValue('miscRegexpString')), + dateRegexp: new RegExp (this.getSettingValue('dateRegexpString')), + discussWithRegexp: new RegExp (this.getSettingValue('discussWithRegexpString')), + waitingForRegexp: new RegExp (this.getSettingValue('waitingForRegexpString')), + promisedToRegexp: new RegExp (this.getSettingValue('promisedToRegexpString')), + somedayMaybeRegexp: new RegExp (this.getSettingValue('somedayMaybeRegexpString')), + excludePath: this.getSettingValue('excludePath'), + excludeFilenameFragment: this.getSettingValue('excludeFilenameFragment').toLowerCase(), + }; + } + + async onload(): Promise { + console.log('loading plugin'); + + await this.loadSettings(); + + this.todoIndex = new TodoIndex(this.app.vault, this.tick.bind(this),this.getTodoItemIndexProps()); + + this.registerView(VIEW_TYPE_TODO, (leaf: WorkspaceLeaf) => { + const todos: TodoItem[] = []; + const props = { + todos: todos, + openFile: (filePath: string) => { + const file = this.app.vault.getAbstractFileByPath(filePath) as TFile; + this.app.workspace.splitActiveLeaf().openFile(file); + }, + toggleTodo: (todo: TodoItem, newStatus: TodoItemStatus) => { + this.todoIndex.setStatus(todo, newStatus); + }, + isInboxVisible: this.getSettingValue('isInboxVisible'), + isAgingVisible: this.getSettingValue('isAgingVisible'), + isTodayVisible: this.getSettingValue('isTodayVisible'), + isScheduledVisible: this.getSettingValue('isScheduledVisible'), + isStakeholderVisible: this.getSettingValue('isStakeholderVisible'), + isSomedayVisible: this.getSettingValue('isSomedayVisible'), + inboxTooltip: this.getSettingValue('inboxTooltip'), + agingTooltip: this.getSettingValue('agingTooltip'), + todayTooltip: this.getSettingValue('todayTooltip'), + scheduledTooltip: this.getSettingValue('scheduledTooltip'), + stakeholderTooltip: this.getSettingValue('stakeholderTooltip'), + somedayTooltip: this.getSettingValue('somedayTooltip'), + }; + this.view = new TodoItemView(leaf, props); + return this.view; + }); + + this.addSettingTab(new ActionTrackerSettingTab(this.app, this)); + + if (this.app.workspace.layoutReady) { + this.initLeaf(); + await this.prepareIndex(); + } else { + this.registerEvent(this.app.workspace.on('layout-ready', this.initLeaf.bind(this))); + this.registerEvent(this.app.workspace.on('layout-ready', async () => await this.prepareIndex())); + } + } + + onunload(): void { + this.app.workspace.getLeavesOfType(VIEW_TYPE_TODO).forEach((leaf) => leaf.detach()); + } + + initLeaf(): void { + if (this.app.workspace.getLeavesOfType(VIEW_TYPE_TODO).length) { + return; + } + this.app.workspace.getRightLeaf(false).setViewState({ + type: VIEW_TYPE_TODO, + }); + } + + async prepareIndex(): Promise { + await this.todoIndex.initialize(); + } + + tick(todos: TodoItem[]): void { + this.view.setProps((currentProps: TodoItemViewProps) => { + return { + ...currentProps, + todos: todos, + }; + }); + } + + async loadSettings() { + this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); + } + + async saveFilterSettings() { + await this.saveData(this.settings); + await this.todoIndex.reloadIndex(this.getTodoItemIndexProps()); + } + + async saveViewDisplaySettings() { + await this.saveData(this.settings); + this.view.setDisplayProps({ + todos: null, + openFile: null, + toggleTodo: null, + isInboxVisible: this.getSettingValue('isInboxVisible'), + isAgingVisible: this.getSettingValue('isAgingVisible'), + isTodayVisible: this.getSettingValue('isTodayVisible'), + isScheduledVisible: this.getSettingValue('isScheduledVisible'), + isStakeholderVisible: this.getSettingValue('isStakeholderVisible'), + isSomedayVisible: this.getSettingValue('isSomedayVisible'), + inboxTooltip: this.getSettingValue('inboxTooltip'), + agingTooltip: this.getSettingValue('agingTooltip'), + todayTooltip: this.getSettingValue('todayTooltip'), + scheduledTooltip: this.getSettingValue('scheduledTooltip'), + stakeholderTooltip: this.getSettingValue('stakeholderTooltip'), + somedayTooltip: this.getSettingValue('somedayTooltip'), + }); + } + + getSettingValue(setting: K): ActionTrackerSettings[K] { + return this.settings[setting] + } +} + + diff --git a/src/model/TodoIndex.ts b/src/model/TodoIndex.ts index 4f4fa19..6c1c0d4 100644 --- a/src/model/TodoIndex.ts +++ b/src/model/TodoIndex.ts @@ -1,126 +1,131 @@ -import { TAbstractFile, TFile, Vault } from 'obsidian'; -import { TodoItem, TodoItemStatus } from '../model/TodoItem'; -import { TodoParser } from '../model/TodoParser'; - -export interface TodoItemIndexProps { - personRegexp: RegExp; - projectRegexp: RegExp; - miscRegexp: RegExp; - dateRegexp: RegExp; - discussWithRegexp: RegExp; - waitingForRegexp: RegExp; - promisedToRegexp: RegExp; - somedayMaybeRegexp: RegExp; -} - -export class TodoIndex { - private vault: Vault; - private todos: Map; - private listeners: ((todos: TodoItem[]) => void)[]; - private props: TodoItemIndexProps; - - constructor(vault: Vault, listener: (todos: TodoItem[]) => void, props: TodoItemIndexProps) { - this.props = props; - this.vault = vault; - this.todos = new Map(); - this.listeners = [listener]; - } - - async reloadIndex(props: TodoItemIndexProps) { - this.props = props; - await this.initialize(); - } - - async initialize(): Promise { - // TODO: persist index & last sync timestamp; only parse files that changed since then. - const todoMap = new Map(); - let numberOfTodos = 0; - const timeStart = new Date().getTime(); - - const markdownFiles = this.vault.getMarkdownFiles(); - for (const file of markdownFiles) { - const todos = await this.parseTodosInFile(file); - numberOfTodos += todos.length; - if (todos.length > 0) { - todoMap.set(file.path, todos); - } - } - - const totalTimeMs = new Date().getTime() - timeStart; - console.log( - `[obsidian-stakeholder_action-plugin] Parsed ${numberOfTodos} TODOs from ${markdownFiles.length} markdown files in (${ - totalTimeMs / 1000.0 - }s)`, - ); - this.todos = todoMap; - this.registerEventHandlers(); - this.invokeListeners(); - } - - setStatus(todo: TodoItem, newStatus: TodoItemStatus): void { - const file = this.vault.getAbstractFileByPath(todo.sourceFilePath) as TFile; - const fileContents = this.vault.read(file); - fileContents.then((c: string) => { - const newTodo = `[${newStatus === TodoItemStatus.Done ? 'x' : ' '}] ${todo.description}`; - const newContents = c.substring(0, todo.startIndex) + newTodo + c.substring(todo.startIndex + todo.length); - this.vault.modify(file, newContents); - }); - } - - private indexAbstractFile(file: TAbstractFile) { - if (!(file instanceof TFile)) { - return; - } - this.indexFile(file as TFile); - } - - private indexFile(file: TFile) { - this.parseTodosInFile(file).then((todos) => { - this.todos.set(file.path, todos); - this.invokeListeners(); - }); - } - - private clearIndex(path: string, silent = false) { - this.todos.delete(path); - if (!silent) { - this.invokeListeners(); - } - } - - public setProps(setter: (currentProps: TodoItemIndexProps) => TodoItemIndexProps): void { - this.props = setter(this.props); - //do I need to do anything else?? - } - - private async parseTodosInFile(file: TFile): Promise { - // TODO: Does it make sense to index completed TODOs at all? - const todoParser = new TodoParser(this.props); - const fileContents = await this.vault.cachedRead(file); - return todoParser - .parseTasks(file.path, fileContents) - .then((todos) => todos.filter((todo) => todo.status === TodoItemStatus.Todo)); - } - - private registerEventHandlers() { - this.vault.on('create', (file: TAbstractFile) => { - this.indexAbstractFile(file); - }); - this.vault.on('modify', (file: TAbstractFile) => { - this.indexAbstractFile(file); - }); - this.vault.on('delete', (file: TAbstractFile) => { - this.clearIndex(file.path); - }); - // We could simply change the references to the old path, but parsing again does the trick as well - this.vault.on('rename', (file: TAbstractFile, oldPath: string) => { - this.clearIndex(oldPath); - this.indexAbstractFile(file); - }); - } - - private invokeListeners() { - const todos = ([] as TodoItem[]).concat(...Array.from(this.todos.values())); - this.listeners.forEach((listener) => listener(todos)); - } -} +import { TAbstractFile, TFile, Vault } from 'obsidian'; +import { TodoItem, TodoItemStatus } from '../model/TodoItem'; +import { TodoParser } from '../model/TodoParser'; + +export interface TodoItemIndexProps { + personRegexp: RegExp; + projectRegexp: RegExp; + miscRegexp: RegExp; + dateRegexp: RegExp; + discussWithRegexp: RegExp; + waitingForRegexp: RegExp; + promisedToRegexp: RegExp; + somedayMaybeRegexp: RegExp; + excludePath: string; + excludeFilenameFragment: string; +} + +export class TodoIndex { + private vault: Vault; + private todos: Map; + private listeners: ((todos: TodoItem[]) => void)[]; + private props: TodoItemIndexProps; + + constructor(vault: Vault, listener: (todos: TodoItem[]) => void, props: TodoItemIndexProps) { + this.props = props; + this.vault = vault; + this.todos = new Map(); + this.listeners = [listener]; + } + + async reloadIndex(props: TodoItemIndexProps) { + this.props = props; + await this.initialize(); + } + + async initialize(): Promise { + // TODO: persist index & last sync timestamp; only parse files that changed since then. + const todoMap = new Map(); + let numberOfTodos = 0; + const timeStart = new Date().getTime(); + + const markdownFiles = this.vault.getMarkdownFiles(); + for (const file of markdownFiles) { + if(!(this.props.excludePath!='' && file.path.startsWith(this.props.excludePath)) && + !(this.props.excludeFilenameFragment!='' && file.path.toLowerCase().includes(this.props.excludeFilenameFragment))) { + const todos = await this.parseTodosInFile(file); + numberOfTodos += todos.length; + if (todos.length > 0) { + todoMap.set(file.path, todos); + } + } + } + + const totalTimeMs = new Date().getTime() - timeStart; + console.log( + `[obsidian-stakeholder_action-plugin] Parsed ${numberOfTodos} TODOs from ${markdownFiles.length} markdown files in (${ + totalTimeMs / 1000.0 + }s)`, + ); + this.todos = todoMap; + this.registerEventHandlers(); + this.invokeListeners(); + } + + setStatus(todo: TodoItem, newStatus: TodoItemStatus): void { + const file = this.vault.getAbstractFileByPath(todo.sourceFilePath) as TFile; + const fileContents = this.vault.read(file); + fileContents.then((c: string) => { + const newTodo = `[${newStatus === TodoItemStatus.Done ? 'x' : ' '}] ${todo.description}`; + const newContents = c.substring(0, todo.startIndex) + newTodo + c.substring(todo.startIndex + todo.length); + this.vault.modify(file, newContents); + }); + } + + private indexAbstractFile(file: TAbstractFile) { + if (!(file instanceof TFile)) { + return; + } + this.indexFile(file as TFile); + } + + private indexFile(file: TFile) { + this.parseTodosInFile(file).then((todos) => { + this.todos.set(file.path, todos); + this.invokeListeners(); + }); + } + + private clearIndex(path: string, silent = false) { + this.todos.delete(path); + if (!silent) { + this.invokeListeners(); + } + } + + public setProps(setter: (currentProps: TodoItemIndexProps) => TodoItemIndexProps): void { + this.props = setter(this.props); + //do I need to do anything else?? + } + + private async parseTodosInFile(file: TFile): Promise { + // TODO: Does it make sense to index completed TODOs at all? + const todoParser = new TodoParser(this.props); + const fileContents = await this.vault.cachedRead(file); + return todoParser + .parseTasks(file.path, fileContents) + .then((todos) => todos.filter((todo) => todo.status === TodoItemStatus.Todo)); + } + + private registerEventHandlers() { + this.vault.on('create', (file: TAbstractFile) => { + this.indexAbstractFile(file); + }); + this.vault.on('modify', (file: TAbstractFile) => { + this.indexAbstractFile(file); + }); + this.vault.on('delete', (file: TAbstractFile) => { + this.clearIndex(file.path); + }); + // We could simply change the references to the old path, but parsing again does the trick as well + this.vault.on('rename', (file: TAbstractFile, oldPath: string) => { + this.clearIndex(oldPath); + this.indexAbstractFile(file); + }); + } + + private invokeListeners() { + const todos = ([] as TodoItem[]).concat(...Array.from(this.todos.values())); + this.listeners.forEach((listener) => listener(todos)); + } +} diff --git a/src/model/TodoParser.test.ts b/src/model/TodoParser.test.ts index 07d362c..973b4df 100644 --- a/src/model/TodoParser.test.ts +++ b/src/model/TodoParser.test.ts @@ -1,67 +1,69 @@ -import { TodoItemStatus } from './TodoItem'; -import { TodoParser } from './TodoParser'; -import { TodoItemIndexProps} from '../model/TodoIndex' - -const props = { - personRegexp: new RegExp('\\[{2}(People\\/*.)\\]{2}'), - projectRegexp: new RegExp('\\[{2}(Projects\\/*.)\\]{2}'), - miscRegexp: new RegExp('(.*)'), - dateRegexp: new RegExp('#(\\d{4}\\/\\d{2}\\/\\d{2})'), - discussWithRegexp: new RegExp('#(discussWith)'), - waitingForRegexp: new RegExp('#(waitingFor)'), - promisedToRegexp: new RegExp('#(promisedTo)'), - somedayMaybeRegexp: new RegExp('#(someday)') -} -const todoParser = new TodoParser(props); - -test('parsing an outstanding todo', async () => { - const contents = `- [ ] This is something that needs doing`; - const todos = await todoParser.parseTasks('/', contents); - const todo = todos[0]; - expect(todo.startIndex).toEqual(2); - expect(todo.length).toEqual(38); - expect(todo.sourceFilePath).toEqual('/'); - expect(todo.status).toEqual(TodoItemStatus.Todo); - expect(todo.description).toEqual('This is something that needs doing'); - expect(todo.actionDate).toBeUndefined(); - expect(todo.isSomedayMaybeNote).toEqual(false); -}); - -test('parsing a completed todo', async () => { - const contents = `- [x] This is something that has been completed`; - const todos = await todoParser.parseTasks('/', contents); - const todo = todos[0]; - expect(todo.startIndex).toEqual(2); - expect(todo.length).toEqual(45); - expect(todo.sourceFilePath).toEqual('/'); - expect(todo.status).toEqual(TodoItemStatus.Done); - expect(todo.description).toEqual('This is something that has been completed'); - expect(todo.actionDate).toBeUndefined(); - expect(todo.isSomedayMaybeNote).toEqual(false); -}); - -test('parsing an outstanding todo with a specific action date', async () => { - const contents = `- [ ] This is something that needs doing #2021/02/16`; - const todos = await todoParser.parseTasks('/', contents); - const todo = todos[0]; - expect(todo.startIndex).toEqual(2); - expect(todo.length).toEqual(50); - expect(todo.sourceFilePath).toEqual('/'); - expect(todo.status).toEqual(TodoItemStatus.Todo); - expect(todo.description).toEqual('This is something that needs doing #2021/02/16'); - expect(todo.actionDate).toEqual(new Date('2021-02-16')); - expect(todo.isSomedayMaybeNote).toEqual(false); -}); - -test('parsing an outstanding someday/maybe todo', async () => { - const contents = `- [ ] This is something that needs doing #someday`; - const todos = await todoParser.parseTasks('/', contents); - const todo = todos[0]; - expect(todo.startIndex).toEqual(2); - expect(todo.length).toEqual(47); - expect(todo.sourceFilePath).toEqual('/'); - expect(todo.status).toEqual(TodoItemStatus.Todo); - expect(todo.description).toEqual('This is something that needs doing #someday'); - expect(todo.actionDate).toBeUndefined(); - expect(todo.isSomedayMaybeNote).toEqual(true); -}); +import { TodoItemStatus } from './TodoItem'; +import { TodoParser } from './TodoParser'; +import { TodoItemIndexProps} from '../model/TodoIndex' + +const props = { + personRegexp: new RegExp('\\[{2}(People\\/*.)\\]{2}'), + projectRegexp: new RegExp('\\[{2}(Projects\\/*.)\\]{2}'), + miscRegexp: new RegExp('(.*)'), + dateRegexp: new RegExp('#(\\d{4}\\/\\d{2}\\/\\d{2})'), + discussWithRegexp: new RegExp('#(discussWith)'), + waitingForRegexp: new RegExp('#(waitingFor)'), + promisedToRegexp: new RegExp('#(promisedTo)'), + somedayMaybeRegexp: new RegExp('#(someday)'), + excludePath: '', + excludeFilenameFragment: '', +} +const todoParser = new TodoParser(props); + +test('parsing an outstanding todo', async () => { + const contents = `- [ ] This is something that needs doing`; + const todos = await todoParser.parseTasks('/', contents); + const todo = todos[0]; + expect(todo.startIndex).toEqual(2); + expect(todo.length).toEqual(38); + expect(todo.sourceFilePath).toEqual('/'); + expect(todo.status).toEqual(TodoItemStatus.Todo); + expect(todo.description).toEqual('This is something that needs doing'); + expect(todo.actionDate).toBeUndefined(); + expect(todo.isSomedayMaybeNote).toEqual(false); +}); + +test('parsing a completed todo', async () => { + const contents = `- [x] This is something that has been completed`; + const todos = await todoParser.parseTasks('/', contents); + const todo = todos[0]; + expect(todo.startIndex).toEqual(2); + expect(todo.length).toEqual(45); + expect(todo.sourceFilePath).toEqual('/'); + expect(todo.status).toEqual(TodoItemStatus.Done); + expect(todo.description).toEqual('This is something that has been completed'); + expect(todo.actionDate).toBeUndefined(); + expect(todo.isSomedayMaybeNote).toEqual(false); +}); + +test('parsing an outstanding todo with a specific action date', async () => { + const contents = `- [ ] This is something that needs doing #2021/02/16`; + const todos = await todoParser.parseTasks('/', contents); + const todo = todos[0]; + expect(todo.startIndex).toEqual(2); + expect(todo.length).toEqual(50); + expect(todo.sourceFilePath).toEqual('/'); + expect(todo.status).toEqual(TodoItemStatus.Todo); + expect(todo.description).toEqual('This is something that needs doing #2021/02/16'); + expect(todo.actionDate).toEqual(new Date('2021-02-16')); + expect(todo.isSomedayMaybeNote).toEqual(false); +}); + +test('parsing an outstanding someday/maybe todo', async () => { + const contents = `- [ ] This is something that needs doing #someday`; + const todos = await todoParser.parseTasks('/', contents); + const todo = todos[0]; + expect(todo.startIndex).toEqual(2); + expect(todo.length).toEqual(47); + expect(todo.sourceFilePath).toEqual('/'); + expect(todo.status).toEqual(TodoItemStatus.Todo); + expect(todo.description).toEqual('This is something that needs doing #someday'); + expect(todo.actionDate).toBeUndefined(); + expect(todo.isSomedayMaybeNote).toEqual(true); +}); diff --git a/src/settings.ts b/src/settings.ts index 5a8ce76..83cbec7 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,318 +1,357 @@ -import {App, PluginSettingTab, Setting} from 'obsidian'; -import type ActionTrackerPlugin from "./main"; - -export interface ActionTrackerSettings { - personRegexpString: string, - projectRegexpString: string, - miscRegexpString: string, - dateRegexpString: string, - discussWithRegexpString: string, - waitingForRegexpString: string, - promisedToRegexpString: string, - somedayMaybeRegexpString: string, - isInboxVisible: boolean, - isAgingVisible: boolean, - isTodayVisible: boolean, - isScheduledVisible: boolean, - isStakeholderVisible: boolean, - isSomedayVisible: boolean, - inboxTooltip: string, - agingTooltip: string, - todayTooltip: string, - scheduledTooltip: string, - stakeholderTooltip: string, - somedayTooltip: string, -} - -export const DEFAULT_SETTINGS: ActionTrackerSettings = { - personRegexpString: '\\[{2}People\\/(.*?)\\]{2}', - projectRegexpString: '\\[{2}Projects\\/(.*?)\\]{2}', - miscRegexpString: '', - dateRegexpString: '#(\\d{4})\\/(\\d{2})\\/(\\d{2})', - discussWithRegexpString: '#(discussWith)', - waitingForRegexpString: '#(waitingFor)', - promisedToRegexpString: '#(promisedTo)', - somedayMaybeRegexpString: '#(someday)', - isInboxVisible: true, - isAgingVisible: true, - isTodayVisible: true, - isScheduledVisible: true, - isStakeholderVisible: true, - isSomedayVisible: true, - inboxTooltip: 'Inbox: No date set, no stakeholder action set, not a someday / maybe item.', - agingTooltip: 'Aging', - todayTooltip: 'Scheduled for Today', - scheduledTooltip: 'Scheduled for a future date', - stakeholderTooltip: 'Stakeholder and Project actions: discussWith, promisedTo, waitingFor. Only items that have a valid project or person will show up here. Stakeholder actions without project or person are in the Inbox.', - somedayTooltip: 'Tagged as Someday / Maybe', -} - -export class ActionTrackerSettingTab extends PluginSettingTab { - plugin: ActionTrackerPlugin; - - constructor(app: App, plugin: ActionTrackerPlugin) { - super(app, plugin); - this.plugin = plugin; - } - - display(): void { - let {containerEl} = this; - - this.containerEl.empty(); - - this.containerEl.createEl('h1', {text: 'Selectors'}); - this.containerEl.createEl('p', {text: 'Selectors are regular expressions that capture specific part of an action. ' + - 'These values control which actions to list in each view, and to filter those lists using the search at the top of the O-GTD pane.'}); - - this.containerEl.createEl('h3', {text: 'Snipets'}); - this.containerEl.createEl('p', {text: 'Use these selectors to capture specific information from your action item. ' + - 'You can filter view results with these using the search box at the top. ' + - 'The "Stakeholder and projects actions" view only shows actions ' + - 'that have either a Project or an Action Party. '}); - this.containerEl.createEl('p', {text: 'The patterns will only capture the first match. If you mention multiple ' + - 'people and projects in a TODO, you must place the action party first. For example:'}); - this.containerEl.createEl('p', {text: '[ ] #discussWith [[People/Catherine]] what present to buy for [[People/Kate]] as a recognition ' + - 'for achievements on [[Project/Secret campaign]].'}); - this.containerEl.createEl('p', {text: 'Catherine will be identified as the Action Party and "Secret campaign" as the Project.'}); - new Setting(containerEl) - .setName('Action Party') - .setDesc('This is the regular expression to identify the action party in the action. Used for filtering todos by person.') - .addText(text => text - .setPlaceholder('\\[{2}People\\/(.*?)\\]{2}') - .setValue(this.plugin.settings.personRegexpString) - .onChange(async (value) => { - this.plugin.settings.personRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - new Setting(containerEl) - .setName('Project') - .setDesc('This is the regular expression to identify the project in the action. Used for filtering todos by project name.') - .addText(text => text - .setPlaceholder('\\[{2}Projects\\/(.*?)\\]{2}') - .setValue(this.plugin.settings.projectRegexpString) - .onChange(async (value) => { - this.plugin.settings.projectRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - new Setting(containerEl) - .setName('Miscellaneous') - .setDesc('This is the regular expression to capture whatever other part of the action you prefer to capture. ' + - 'This snipet will only effect filtering when using the search box at the top of the O-GTD panel. The default setting is to capture nothing. ' + - 'Should you, for example, want to capture the whole action line, change the pattern to: (.*)') - .addText(text => text - .setPlaceholder('(.*)') - .setValue(this.plugin.settings.miscRegexpString) - .onChange(async (value) => { - this.plugin.settings.miscRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - this.containerEl.createEl('h3', {text: 'Date'}); - this.containerEl.createEl('p', {text: 'This is the actions\'s due date. If set, your action will show up in one of the date filtered views: ' + - 'Aging (date is overdue), Today (date is today), Scheduled (future date).'}); - new Setting(containerEl) - .setName('Date') - .setDesc('This is the regular expression to get the date for an action. You have two options. 1) If your RegExp captures 3 values, then '+ - 'the first must be the year (yyyy), the sceond the month (mm), the third the day (dd). ' + - '2) If your RegExp captures a single value, then it must be a valid date based on your regional settings. For example: ' + - '#(\\d{4}\\-\\d{2}\\-\\d{2}) is captures the following date yyyy-mm-dd') - .addText(text => text - .setPlaceholder('#(\\d{4})\\/(\\d{2})\\/(\\d{2})') - .setValue(this.plugin.settings.dateRegexpString) - .onChange(async (value) => { - this.plugin.settings.dateRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - this.containerEl.createEl('h3', {text: 'Stakeholder-action tags'}); - this.containerEl.createEl('p', {text: 'These three tags allow you to qualify the type of stakeholder action you intend to take. ' + - 'In the "Stakeholder and project actions" view items will be pre-sorted ' + - 'with #discussWith first, #waitingFor second, and #promisedTo third. I can imagine situations ' + - 'when you repurpose these to list actions based on priority: #high, #medium, #low by changing the regular expressions.'}); - new Setting(containerEl) - .setName('Discuss With tag') - .setDesc('This is the regular expression to identify topics you want to discuss with someone.') - .addText(text => text - .setPlaceholder('#(discussWith)') - .setValue(this.plugin.settings.discussWithRegexpString) - .onChange(async (value) => { - this.plugin.settings.discussWithRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - new Setting(containerEl) - .setName('Waiting For') - .setDesc('This is the regular expression to identify actions which you are waiting for some to complete.') - .addText(text => text - .setPlaceholder('#(waitingFor)') - .setValue(this.plugin.settings.waitingForRegexpString) - .onChange(async (value) => { - this.plugin.settings.waitingForRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - new Setting(containerEl) - .setName('Promised To') - .setDesc('This is the regular expression to identify promises you have made to someone.') - .addText(text => text - .setPlaceholder('#(promisedTo)') - .setValue(this.plugin.settings.promisedToRegexpString) - .onChange(async (value) => { - this.plugin.settings.promisedToRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - this.containerEl.createEl('h3', {text: 'Someday/Maybe tag'}); - this.containerEl.createEl('p', {text: 'Use this tag to mark actions that are deliberately without a deadline, such as ' + - 'items on your bucket list. These actions will show up in the "Someday/Maybe view. ' + - 'Note, that actions without a valid tag and without a deadline will show up in the Inbox.'}); - new Setting(containerEl) - .setName('Someday Maybe regexp pattern') - .setDesc('This is the regular expression to identify the Someday/Maybe tag.') - .addText(text => text - .setPlaceholder('#(someday)') - .setValue(this.plugin.settings.somedayMaybeRegexpString) - .onChange(async (value) => { - this.plugin.settings.somedayMaybeRegexpString = value; - await this.plugin.saveFilterSettings(); - })); - - this.containerEl.createEl('h1', {text: 'View Configuration'}); - this.containerEl.createEl('p', {text: 'You can show/hide specific views based on your needs. ' + - 'As you customize some of the selectors, views may slightly change their meaning. You can update '+ - 'the tooltip text to help you remember your intent with each view.'}); - - this.containerEl.createEl('h3', {text: 'Inbox'}); - new Setting(containerEl) - .setName('Show/hide Inbox') - .addToggle(value => value - .setValue(this.plugin.settings.isInboxVisible) - .onChange(async (value) => { - this.plugin.settings.isInboxVisible = value; - await this.plugin.saveViewDisplaySettings(); - })); - new Setting(containerEl) - .setName('Inbox tooltip') - .addTextArea(text => { - let t = text.setPlaceholder(DEFAULT_SETTINGS.inboxTooltip) - .setValue(this.plugin.settings.inboxTooltip) - .onChange(async (value) => { - this.plugin.settings.inboxTooltip = value; - await this.plugin.saveViewDisplaySettings(); - }); - t.inputEl.setAttr("rows", 4); - }); - - - this.containerEl.createEl('h3', {text: 'Aging'}); - new Setting(containerEl) - .setName('Show/hide Aging') - .addToggle(value => value - .setValue(this.plugin.settings.isAgingVisible) - .onChange(async (value) => { - this.plugin.settings.isAgingVisible = value; - await this.plugin.saveViewDisplaySettings(); - })); - new Setting(containerEl) - .setName('Aging tooltip') - .addTextArea(text => { - let t = text.setPlaceholder(DEFAULT_SETTINGS.agingTooltip) - .setValue(this.plugin.settings.agingTooltip) - .onChange(async (value) => { - this.plugin.settings.agingTooltip = value; - await this.plugin.saveViewDisplaySettings(); - }); - t.inputEl.setAttr("rows", 4); - }); - - - this.containerEl.createEl('h3', {text: 'Today'}); - new Setting(containerEl) - .setName('Show/hide Today') - .addToggle(value => value - .setValue(this.plugin.settings.isTodayVisible) - .onChange(async (value) => { - this.plugin.settings.isTodayVisible = value; - await this.plugin.saveViewDisplaySettings(); - })); - new Setting(containerEl) - .setName('Today tooltip') - .addTextArea(text => { - let t = text.setPlaceholder(DEFAULT_SETTINGS.todayTooltip) - .setValue(this.plugin.settings.todayTooltip) - .onChange(async (value) => { - this.plugin.settings.todayTooltip = value; - await this.plugin.saveViewDisplaySettings(); - }); - t.inputEl.setAttr("rows", 4); - }); - - - this.containerEl.createEl('h3', {text: 'Scheduled'}); - new Setting(containerEl) - .setName('Show/hide Scheduled') - .addToggle(value => value - .setValue(this.plugin.settings.isScheduledVisible) - .onChange(async (value) => { - this.plugin.settings.isScheduledVisible = value; - await this.plugin.saveViewDisplaySettings(); - })); - new Setting(containerEl) - .setName('Scheduled tooltip') - .addTextArea(text => { - let t = text.setPlaceholder(DEFAULT_SETTINGS.scheduledTooltip) - .setValue(this.plugin.settings.scheduledTooltip) - .onChange(async (value) => { - this.plugin.settings.scheduledTooltip = value; - await this.plugin.saveViewDisplaySettings(); - }); - t.inputEl.setAttr("rows", 4); - }); - - - this.containerEl.createEl('h3', {text: 'Stakeholder Actions'}); - new Setting(containerEl) - .setName('Show/hide Stakeholder Actions') - .addToggle(value => value - .setValue(this.plugin.settings.isStakeholderVisible) - .onChange(async (value) => { - this.plugin.settings.isStakeholderVisible = value; - await this.plugin.saveViewDisplaySettings(); - })); - new Setting(containerEl) - .setName('Stakeholder Actions tooltip') - .addTextArea(text => { - let t = text.setPlaceholder(DEFAULT_SETTINGS.stakeholderTooltip) - .setValue(this.plugin.settings.stakeholderTooltip) - .onChange(async (value) => { - this.plugin.settings.stakeholderTooltip = value; - await this.plugin.saveViewDisplaySettings(); - }); - t.inputEl.setAttr("rows", 4); - }); - - - this.containerEl.createEl('h3', {text: 'Someday/Maybe'}); - new Setting(containerEl) - .setName('Show/hide Someday/Maybe') - .addToggle(value => value - .setValue(this.plugin.settings.isSomedayVisible) - .onChange(async (value) => { - this.plugin.settings.isSomedayVisible = value; - await this.plugin.saveViewDisplaySettings(); - })); - new Setting(containerEl) - .setName('Someday/Maybe tooltip') - .addTextArea(text => { - let t = text.setPlaceholder(DEFAULT_SETTINGS.somedayTooltip) - .setValue(this.plugin.settings.somedayTooltip) - .onChange(async (value) => { - this.plugin.settings.somedayTooltip = value; - await this.plugin.saveViewDisplaySettings(); - }); - t.inputEl.setAttr("rows", 4); - }); - } +import {App, PluginSettingTab, Setting} from 'obsidian'; +import type ActionTrackerPlugin from "./main"; + +export interface ActionTrackerSettings { + personRegexpString: string, + projectRegexpString: string, + miscRegexpString: string, + dateRegexpString: string, + discussWithRegexpString: string, + waitingForRegexpString: string, + promisedToRegexpString: string, + somedayMaybeRegexpString: string, + excludePath: string, + excludeFilenameFragment: string, + isInboxVisible: boolean, + isAgingVisible: boolean, + isTodayVisible: boolean, + isScheduledVisible: boolean, + isStakeholderVisible: boolean, + isSomedayVisible: boolean, + inboxTooltip: string, + agingTooltip: string, + todayTooltip: string, + scheduledTooltip: string, + stakeholderTooltip: string, + somedayTooltip: string, +} + +export const DEFAULT_SETTINGS: ActionTrackerSettings = { + personRegexpString: '\\[{2}People\\/(.*?)\\]{2}', + projectRegexpString: '\\[{2}Projects\\/(.*?)\\]{2}', + miscRegexpString: '', + dateRegexpString: '#(\\d{4})\\/(\\d{2})\\/(\\d{2})', + discussWithRegexpString: '#(discussWith)', + waitingForRegexpString: '#(waitingFor)', + promisedToRegexpString: '#(promisedTo)', + somedayMaybeRegexpString: '#(someday)', + excludePath: '', + excludeFilenameFragment: '', + isInboxVisible: true, + isAgingVisible: true, + isTodayVisible: true, + isScheduledVisible: true, + isStakeholderVisible: true, + isSomedayVisible: true, + inboxTooltip: 'Inbox: No date set, no stakeholder action set, not a someday / maybe item.', + agingTooltip: 'Aging', + todayTooltip: 'Scheduled for Today', + scheduledTooltip: 'Scheduled for a future date', + stakeholderTooltip: 'Stakeholder and Project actions: discussWith, promisedTo, waitingFor. Only items that have a valid project or person will show up here. Stakeholder actions without project or person are in the Inbox.', + somedayTooltip: 'Tagged as Someday / Maybe', +} + +export class ActionTrackerSettingTab extends PluginSettingTab { + plugin: ActionTrackerPlugin; + + constructor(app: App, plugin: ActionTrackerPlugin) { + super(app, plugin); + this.plugin = plugin; + } + + display(): void { + let {containerEl} = this; + + this.containerEl.empty(); + + this.containerEl.createEl('h1', {text: 'Selectors'}); + this.containerEl.createEl('p', {text: 'Selectors are regular expressions that capture specific part of an action. ' + + 'These values control which actions to list in each view, and to filter those lists using the search at the top of the O-GTD pane.'}); + + this.containerEl.createEl('h3', {text: 'Snipets'}); + this.containerEl.createEl('p', {text: 'Use these selectors to capture specific information from your action item. ' + + 'You can filter view results with these using the search box at the top. ' + + 'The "Stakeholder and projects actions" view only shows actions ' + + 'that have either a Project or an Action Party. '}); + this.containerEl.createEl('p', {text: 'The patterns will only capture the first match. If you mention multiple ' + + 'people and projects in a TODO, you must place the action party first. For example:'}); + this.containerEl.createEl('p', {text: '[ ] #discussWith [[People/Catherine]] what present to buy for [[People/Kate]] as a recognition ' + + 'for achievements on [[Project/Secret campaign]].'}); + this.containerEl.createEl('p', {text: 'Catherine will be identified as the Action Party and "Secret campaign" as the Project.'}); + new Setting(containerEl) + .setName('Action Party') + .setDesc('This is the regular expression to identify the action party in the action. Used for filtering todos by person.') + .addText(text => text + .setPlaceholder('\\[{2}People\\/(.*?)\\]{2}') + .setValue(this.plugin.settings.personRegexpString) + .onChange(async (value) => { + this.plugin.settings.personRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + new Setting(containerEl) + .setName('Project') + .setDesc('This is the regular expression to identify the project in the action. Used for filtering todos by project name.') + .addText(text => text + .setPlaceholder('\\[{2}Projects\\/(.*?)\\]{2}') + .setValue(this.plugin.settings.projectRegexpString) + .onChange(async (value) => { + this.plugin.settings.projectRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + new Setting(containerEl) + .setName('Miscellaneous') + .setDesc('This is the regular expression to capture whatever other part of the action you prefer to capture. ' + + 'This snipet will only effect filtering when using the search box at the top of the O-GTD panel. The default setting is to capture nothing. ' + + 'Should you, for example, want to capture the whole action line, change the pattern to: (.*)') + .addText(text => text + .setPlaceholder('(.*)') + .setValue(this.plugin.settings.miscRegexpString) + .onChange(async (value) => { + this.plugin.settings.miscRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + this.containerEl.createEl('h3', {text: 'Date'}); + this.containerEl.createEl('p', {text: 'This is the actions\'s due date. If set, your action will show up in one of the date filtered views: ' + + 'Aging (date is overdue), Today (date is today), Scheduled (future date).'}); + new Setting(containerEl) + .setName('Date') + .setDesc('This is the regular expression to get the date for an action. You have two options. 1) If your RegExp captures 3 values, then '+ + 'the first must be the year (yyyy), the sceond the month (mm), the third the day (dd). ' + + '2) If your RegExp captures a single value, then it must be a valid date based on your regional settings. For example: ' + + '#(\\d{4}\\-\\d{2}\\-\\d{2}) is captures the following date yyyy-mm-dd') + .addText(text => text + .setPlaceholder('#(\\d{4})\\/(\\d{2})\\/(\\d{2})') + .setValue(this.plugin.settings.dateRegexpString) + .onChange(async (value) => { + this.plugin.settings.dateRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + this.containerEl.createEl('h3', {text: 'Stakeholder-action tags'}); + this.containerEl.createEl('p', {text: 'These three tags allow you to qualify the type of stakeholder action you intend to take. ' + + 'In the "Stakeholder and project actions" view items will be pre-sorted ' + + 'with #discussWith first, #waitingFor second, and #promisedTo third. I can imagine situations ' + + 'when you repurpose these to list actions based on priority: #high, #medium, #low by changing the regular expressions.'}); + new Setting(containerEl) + .setName('Discuss With tag') + .setDesc('This is the regular expression to identify topics you want to discuss with someone.') + .addText(text => text + .setPlaceholder('#(discussWith)') + .setValue(this.plugin.settings.discussWithRegexpString) + .onChange(async (value) => { + this.plugin.settings.discussWithRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + new Setting(containerEl) + .setName('Waiting For') + .setDesc('This is the regular expression to identify actions which you are waiting for some to complete.') + .addText(text => text + .setPlaceholder('#(waitingFor)') + .setValue(this.plugin.settings.waitingForRegexpString) + .onChange(async (value) => { + this.plugin.settings.waitingForRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + new Setting(containerEl) + .setName('Promised To') + .setDesc('This is the regular expression to identify promises you have made to someone.') + .addText(text => text + .setPlaceholder('#(promisedTo)') + .setValue(this.plugin.settings.promisedToRegexpString) + .onChange(async (value) => { + this.plugin.settings.promisedToRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + this.containerEl.createEl('h3', {text: 'Someday/Maybe tag'}); + this.containerEl.createEl('p', {text: 'Use this tag to mark actions that are deliberately without a deadline, such as ' + + 'items on your bucket list. These actions will show up in the "Someday/Maybe view. ' + + 'Note, that actions without a valid tag and without a deadline will show up in the Inbox.'}); + new Setting(containerEl) + .setName('Someday Maybe regexp pattern') + .setDesc('This is the regular expression to identify the Someday/Maybe tag.') + .addText(text => text + .setPlaceholder('#(someday)') + .setValue(this.plugin.settings.somedayMaybeRegexpString) + .onChange(async (value) => { + this.plugin.settings.somedayMaybeRegexpString = value; + await this.plugin.saveFilterSettings(); + })); + + + + + this.containerEl.createEl('h1', {text: 'Exclusions'}); + this.containerEl.createEl('p', {text: 'Settings to define which actions to exclude from the view.'}); + new Setting(containerEl) + .setName('Exclude path') + .setDesc('Intended for excluding your Templates folder. The files in this folder and all sub-folders will be excluded by Obsidian-GTD. The value given in this setting '+ + 'is matched to the beginning of the filepath using .startsWith(). If you set this value to Temp, then '+ + 'all files on the filepath startring with the string given will be excluded. To stick with our example Temp will exclude all of the following folders: ' + + 'Templates/, Template/, Temp/, etc. but will not exclude templates/ or temp/... Exclude folder names are case sensitive.') + .addText(text => text + .setPlaceholder('Templates/') + .setValue(this.plugin.settings.excludePath) + .onChange(async (value) => { + this.plugin.settings.excludePath = value; + await this.plugin.saveFilterSettings(); + })); + + new Setting(containerEl) + .setName('Exclude filename fragment') + .setDesc('Intended for filtering out a special type of file that can be identified based on filename, such as checklists. If you name all your checklists '+ + 'with the word checklist in the file (e.g. "Holiday checklist.md", "Blog post checklist.md", "Checklist for Christmas.md", etc.), ' + + 'those files will all be excluded. Uses .toLowerCase().includes() to filter elements, i.e. the filename fragment is '+ + 'case insensitive') + .addText(text => text + .setPlaceholder('checklist') + .setValue(this.plugin.settings.excludeFilenameFragment) + .onChange(async (value) => { + this.plugin.settings.excludeFilenameFragment = value; + await this.plugin.saveFilterSettings(); + })); + + + + this.containerEl.createEl('h1', {text: 'Configure Views'}); + this.containerEl.createEl('p', {text: 'You can show/hide specific views based on your needs. ' + + 'As you customize some of the selectors, views may slightly change their meaning. You can update '+ + 'the tooltip text to help you remember your intent with each view.'}); + + this.containerEl.createEl('h3', {text: 'Inbox'}); + new Setting(containerEl) + .setName('Show/hide Inbox') + .addToggle(value => value + .setValue(this.plugin.settings.isInboxVisible) + .onChange(async (value) => { + this.plugin.settings.isInboxVisible = value; + await this.plugin.saveViewDisplaySettings(); + })); + new Setting(containerEl) + .setName('Inbox tooltip') + .addTextArea(text => { + let t = text.setPlaceholder(DEFAULT_SETTINGS.inboxTooltip) + .setValue(this.plugin.settings.inboxTooltip) + .onChange(async (value) => { + this.plugin.settings.inboxTooltip = value; + await this.plugin.saveViewDisplaySettings(); + }); + t.inputEl.setAttr("rows", 4); + }); + + + this.containerEl.createEl('h3', {text: 'Aging'}); + new Setting(containerEl) + .setName('Show/hide Aging') + .addToggle(value => value + .setValue(this.plugin.settings.isAgingVisible) + .onChange(async (value) => { + this.plugin.settings.isAgingVisible = value; + await this.plugin.saveViewDisplaySettings(); + })); + new Setting(containerEl) + .setName('Aging tooltip') + .addTextArea(text => { + let t = text.setPlaceholder(DEFAULT_SETTINGS.agingTooltip) + .setValue(this.plugin.settings.agingTooltip) + .onChange(async (value) => { + this.plugin.settings.agingTooltip = value; + await this.plugin.saveViewDisplaySettings(); + }); + t.inputEl.setAttr("rows", 4); + }); + + + this.containerEl.createEl('h3', {text: 'Today'}); + new Setting(containerEl) + .setName('Show/hide Today') + .addToggle(value => value + .setValue(this.plugin.settings.isTodayVisible) + .onChange(async (value) => { + this.plugin.settings.isTodayVisible = value; + await this.plugin.saveViewDisplaySettings(); + })); + new Setting(containerEl) + .setName('Today tooltip') + .addTextArea(text => { + let t = text.setPlaceholder(DEFAULT_SETTINGS.todayTooltip) + .setValue(this.plugin.settings.todayTooltip) + .onChange(async (value) => { + this.plugin.settings.todayTooltip = value; + await this.plugin.saveViewDisplaySettings(); + }); + t.inputEl.setAttr("rows", 4); + }); + + + this.containerEl.createEl('h3', {text: 'Scheduled'}); + new Setting(containerEl) + .setName('Show/hide Scheduled') + .addToggle(value => value + .setValue(this.plugin.settings.isScheduledVisible) + .onChange(async (value) => { + this.plugin.settings.isScheduledVisible = value; + await this.plugin.saveViewDisplaySettings(); + })); + new Setting(containerEl) + .setName('Scheduled tooltip') + .addTextArea(text => { + let t = text.setPlaceholder(DEFAULT_SETTINGS.scheduledTooltip) + .setValue(this.plugin.settings.scheduledTooltip) + .onChange(async (value) => { + this.plugin.settings.scheduledTooltip = value; + await this.plugin.saveViewDisplaySettings(); + }); + t.inputEl.setAttr("rows", 4); + }); + + + this.containerEl.createEl('h3', {text: 'Stakeholder Actions'}); + new Setting(containerEl) + .setName('Show/hide Stakeholder Actions') + .addToggle(value => value + .setValue(this.plugin.settings.isStakeholderVisible) + .onChange(async (value) => { + this.plugin.settings.isStakeholderVisible = value; + await this.plugin.saveViewDisplaySettings(); + })); + new Setting(containerEl) + .setName('Stakeholder Actions tooltip') + .addTextArea(text => { + let t = text.setPlaceholder(DEFAULT_SETTINGS.stakeholderTooltip) + .setValue(this.plugin.settings.stakeholderTooltip) + .onChange(async (value) => { + this.plugin.settings.stakeholderTooltip = value; + await this.plugin.saveViewDisplaySettings(); + }); + t.inputEl.setAttr("rows", 4); + }); + + + this.containerEl.createEl('h3', {text: 'Someday/Maybe'}); + new Setting(containerEl) + .setName('Show/hide Someday/Maybe') + .addToggle(value => value + .setValue(this.plugin.settings.isSomedayVisible) + .onChange(async (value) => { + this.plugin.settings.isSomedayVisible = value; + await this.plugin.saveViewDisplaySettings(); + })); + new Setting(containerEl) + .setName('Someday/Maybe tooltip') + .addTextArea(text => { + let t = text.setPlaceholder(DEFAULT_SETTINGS.somedayTooltip) + .setValue(this.plugin.settings.somedayTooltip) + .onChange(async (value) => { + this.plugin.settings.somedayTooltip = value; + await this.plugin.saveViewDisplaySettings(); + }); + t.inputEl.setAttr("rows", 4); + }); + } } \ No newline at end of file diff --git a/src/ui/TodoItemView.ts b/src/ui/TodoItemView.ts index 5255f09..5edcb8c 100644 --- a/src/ui/TodoItemView.ts +++ b/src/ui/TodoItemView.ts @@ -1,407 +1,407 @@ -import { ItemView, MarkdownRenderer, WorkspaceLeaf } from 'obsidian'; -import { VIEW_TYPE_TODO } from '../constants'; -import { TodoItem, TodoItemStatus } from '../model/TodoItem'; -import { RenderIcon, Icon } from '../ui/icons'; - -enum TodoItemViewPane { - Aging, - Today, - Scheduled, - Inbox, - Someday, - Stakeholder, -} - -enum TodoSortStates { - None = 0, - DateAsc = 1, - DateDesc = 2, - StakeholderAsc = 3, - StakeholderDesc = 4, - ProjectAsc = 5, - ProjectDesc = 6, - MiscAsc = 7, - MiscDesc = 8, - FullTextAsc = 9, - FullTextDesc = 10, -} - -export interface TodoItemViewProps { - todos: TodoItem[]; - openFile: (filePath: string) => void; - toggleTodo: (todo: TodoItem, newStatus: TodoItemStatus) => void; - isInboxVisible: boolean; - isAgingVisible: boolean; - isTodayVisible: boolean; - isScheduledVisible: boolean; - isStakeholderVisible: boolean; - isSomedayVisible: boolean; - inboxTooltip: string; - agingTooltip: string; - todayTooltip: string; - scheduledTooltip: string; - stakeholderTooltip: string; - somedayTooltip: string; -} - -interface TodoItemViewState { - activePane: TodoItemViewPane; -} - -interface TodoSortState { - state: TodoSortStates, -} - -export class TodoItemView extends ItemView { - private props: TodoItemViewProps; - private state: TodoItemViewState; - private sortState: TodoSortState; - private filter: string; - private filterRegexp: RegExp; - private sortStateCount; - - constructor(leaf: WorkspaceLeaf, props: TodoItemViewProps) { - //debugger; - super(leaf); - this.props = props; - this.state = { - activePane: TodoItemViewPane.Today, - }; - this.sortState = { - state: TodoSortStates.None, - }; - this.sortStateCount = Object.values(TodoSortStates).length/2; - this.filter = ''; - } - - getViewType(): string { - return VIEW_TYPE_TODO; - } - - getDisplayText(): string { - return 'Obsidian GTD'; - } - - getIcon(): string { - return 'checkmark'; - } - - onClose(): Promise { - return Promise.resolve(); - } - - public setDisplayProps(props: TodoItemViewProps) { - this.props.isInboxVisible = props.isInboxVisible; - this.props.isAgingVisible = props.isAgingVisible; - this.props.isTodayVisible = props.isTodayVisible; - this.props.isScheduledVisible = props.isScheduledVisible; - this.props.isStakeholderVisible = props.isStakeholderVisible; - this.props.isSomedayVisible = props.isSomedayVisible; - this.props.inboxTooltip = props.inboxTooltip; - this.props.agingTooltip = props.agingTooltip; - this.props.todayTooltip = props.todayTooltip; - this.props.scheduledTooltip = props.scheduledTooltip; - this.props.stakeholderTooltip = props.stakeholderTooltip; - this.props.somedayTooltip = props.somedayTooltip; - this.render(); - } - - public setProps(setter: (currentProps: TodoItemViewProps) => TodoItemViewProps): void { - this.props = setter(this.props); - this.render(); - } - - private setViewState(newState: TodoItemViewState) { - this.state = newState; - if(newState.activePane == TodoItemViewPane.Aging || newState.activePane == TodoItemViewPane.Scheduled || newState.activePane == TodoItemViewPane.Today) - this.sortState = {state: TodoSortStates.DateAsc}; - else if (newState.activePane == TodoItemViewPane.Stakeholder) - this.sortState = {state: TodoSortStates.StakeholderAsc}; - else - this.sortState = {state: TodoSortStates.FullTextAsc}; - this.render(); - } - - private setSortState(newState: TodoSortState) { - this.sortState = newState; - this.render(); - } - - private setFilter(filter: string) { - this.filter = filter; - this.filterRegexp = new RegExp(filter,'i'); - this.renderViewItemsOnly(); - } - - private renderViewItemsOnly():void { - const container = this.containerEl.children[1].children[0]; - container.children[2].remove(); - container.createDiv('todo-item-view-items', (el) => { - this.renderItems(el); - }); - } - - private render(): void { - const container = this.containerEl.children[1]; - container.empty(); - container.createDiv('todo-item-view-container', (el) => { - el.createDiv('todo-item-view-search', (el) => { - this.renderSearch(el); - }); - el.createDiv('todo-item-view-toolbar', (el) => { - this.renderToolBar(el); - }); - el.createDiv('todo-item-view-items', (el) => { - this.renderItems(el); - }); - }); - } - - private renderSearch(container: HTMLDivElement) { - const activeClass = () => { - if (this.sortState.state == TodoSortStates.None) - return ' none'; - else - return ' active'; - }; - - const sortLabel = (sortState:TodoSortStates) => { - switch (sortState) { - case TodoSortStates.None: - return 'Sort by: none'; - case TodoSortStates.DateDesc: - return 'Sort by: Action Date Descending'; - case TodoSortStates.DateAsc: - return 'Sort by: Action Date Ascending'; - case TodoSortStates.StakeholderDesc: - return 'Sort by: Person Descending'; - case TodoSortStates.StakeholderAsc: - return 'Sort by: Person Ascending'; - case TodoSortStates.ProjectDesc: - return 'Sort by: Project Descending'; - case TodoSortStates.ProjectAsc: - return 'Sort by: Project Ascending'; - case TodoSortStates.MiscDesc: - return 'Sort by: Misc-snippet Descending'; - case TodoSortStates.MiscAsc: - return 'Sort by: Misc-snippet Ascending'; - case TodoSortStates.FullTextDesc: - return 'Sort by: Full Text Descending'; - case TodoSortStates.FullTextAsc: - return 'Sort by: Full Text Ascending'; - } - } - - container.createEl('input', {value: this.filter}, (el) => { - el.addClass('todo-filter-input'); - el.setAttribute('placeholder','proj/person RexExp filter, case insensitive'); - el.onkeyup = (e) => { - this.setFilter((e.target).value); - }; - }); - - container.createDiv(`todo-item-view-sort${activeClass()}`, (el) => { - el.appendChild(RenderIcon(Icon.Sort, sortLabel(this.sortState.state))); - el.onClickEvent((e) => { - const nextSortState = (this.sortState.state + 1) % this.sortStateCount; - this.setSortState({state: nextSortState}); - //(e.target).setAttribute('aria-label',sortLabel(nextSortState)); - }); - }); - } - - private renderToolBar(container: HTMLDivElement) { - const activeClass = (pane: TodoItemViewPane) => { - return pane === this.state.activePane ? ' active' : ''; - }; - - const setActivePane = (pane: TodoItemViewPane) => { - const newState = { - ...this.state, - activePane: pane, - }; - this.setViewState(newState); - }; - - if (this.props.isInboxVisible) - container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Inbox)}`, (el) => { - el.appendChild(RenderIcon(Icon.Inbox, this.props.inboxTooltip)); - el.onClickEvent(() => setActivePane(TodoItemViewPane.Inbox)); - }); - - if (this.props.isAgingVisible) - container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Aging)}`, (el) => { - el.appendChild(RenderIcon(Icon.Aging, this.props.agingTooltip)); - el.onClickEvent(() => setActivePane(TodoItemViewPane.Aging)); - }); - - if (this.props.isTodayVisible) - container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Today)}`, (el) => { - el.appendChild(RenderIcon(Icon.Today, this.props.todayTooltip)); - el.onClickEvent(() => setActivePane(TodoItemViewPane.Today)); - }); - - if (this.props.isScheduledVisible) - container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Scheduled)}`, (el) => { - el.appendChild(RenderIcon(Icon.Scheduled, this.props.scheduledTooltip)); - el.onClickEvent(() => setActivePane(TodoItemViewPane.Scheduled)); - }); - - if (this.props.isStakeholderVisible) - container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Stakeholder)}`, (el) => { - el.appendChild(RenderIcon(Icon.Stakeholder, this.props.stakeholderTooltip)); - el.onClickEvent(() => setActivePane(TodoItemViewPane.Stakeholder)); - }); - - if (this.props.isSomedayVisible) - container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Someday)}`, (el) => { - el.appendChild(RenderIcon(Icon.Someday, this.props.somedayTooltip)); - el.onClickEvent(() => setActivePane(TodoItemViewPane.Someday)); - }); - } - - private renderItems(container: HTMLDivElement) { - const sortView = (a: TodoItem, b: TodoItem) => { - return this.sortView(a,b); - }; - const todosToRender = this.props.todos - .filter(this.filterForState, this); - const sortedTodos = todosToRender.sort(sortView); - sortedTodos - .forEach((todo,index) => { - if(index>0) { - if( (todo.isWaitingForNote && todosToRender[index-1].isDiscussWithNote) || - (todo.isPromisedToNote && - (todosToRender[index-1].isWaitingForNote || todosToRender[index-1].isDiscussWithNote)) || - (!todo.isPromisedToNote && !todo.isWaitingForNote && !todo.isDiscussWithNote && - (todosToRender[index-1].isWaitingForNote || todosToRender[index-1].isDiscussWithNote || todosToRender[index-1].isPromisedToNote)) ) { - container.createEl('hr', {} ,(el) => { - el.addClass('todo-item-view-divider'); - }); - } - } - container.createDiv('todo-item-view-item', (el) => { - el.createDiv('todo-item-view-item-checkbox', (el) => { - el.createEl('input', { type: 'checkbox' }, (el) => { - el.checked = todo.status === TodoItemStatus.Done; - el.onClickEvent(() => { - this.toggleTodo(todo); - }); - }); - }); - el.createDiv('todo-item-view-item-description', (el) => { - MarkdownRenderer.renderMarkdown(todo.description, el, todo.sourceFilePath, this); - }); - el.createDiv('todo-item-view-item-link', (el) => { - el.appendChild(RenderIcon(Icon.Reveal, 'Open file')); - el.onClickEvent(() => { - this.openFile(todo); - }); - }); - }); - }); - } - - private sortView(a: TodoItem, b: TodoItem) { - let sortResult = 0; - switch (this.sortState.state) { - case TodoSortStates.None: - sortResult = 0;break; - case TodoSortStates.DateAsc: - sortResult = a.actionDate < b.actionDate ? -1 : a.actionDate > b.actionDate ? 1 : 0;break; - case TodoSortStates.DateDesc: - sortResult = a.actionDate > b.actionDate ? -1 : a.actionDate < b.actionDate ? 1 : 0;break; - case TodoSortStates.StakeholderAsc: - sortResult = a.person < b.person ? -1 : a.person > b.person ? 1 : 0;break; - case TodoSortStates.StakeholderDesc: - sortResult = a.person > b.person ? -1 : a.person < b.person ? 1 : 0;break; - case TodoSortStates.ProjectAsc: - sortResult = a.project < b.project ? -1 : a.project > b.project ? 1 : 0;break; - case TodoSortStates.ProjectDesc: - sortResult = a.project > b.project ? -1 : a.project < b.project ? 1 : 0;break; - case TodoSortStates.MiscAsc: - sortResult = a.misc < b.misc ? -1 : a.misc > b.misc ? 1 : 0;break; - case TodoSortStates.MiscDesc: - sortResult = a.misc > b.misc ? -1 : a.misc < b.misc ? 1 : 0;break; - case TodoSortStates.FullTextAsc: - sortResult = a.description.toLowerCase() < b.description.toLowerCase() ? -1 : a.description.toLowerCase() > b.description.toLowerCase() ? 1 : 0;break; - case TodoSortStates.FullTextDesc: - sortResult = a.description.toLowerCase() > b.description.toLowerCase() ? -1 : a.description.toLowerCase() < b.description.toLowerCase() ? 1 : 0;break; - } - - if (this.state.activePane == TodoItemViewPane.Stakeholder) { - if (a.isDiscussWithNote && !b.isDiscussWithNote) { - return -1; - } - if (a.isWaitingForNote && !b.isDiscussWithNote && !b.isWaitingForNote) { - return -1; - } - if (a.isPromisedToNote && !b.isDiscussWithNote && !b.isWaitingForNote) { - return -1; - } - if (b.isDiscussWithNote && !a.isDiscussWithNote) { - return 1; - } - if (b.isWaitingForNote && !a.isDiscussWithNote && !a.isWaitingForNote) { - return 1; - } - if (b.isPromisedToNote && !a.isDiscussWithNote && !a.isWaitingForNote) { - return 1; - } - return sortResult; - } - return sortResult; - } - - private filterForState(value: TodoItem, _index: number, _array: TodoItem[]): boolean { - const isPersonMatch = value.person.match(this.filterRegexp) != null; - const isProjectMatch = value.project.match(this.filterRegexp) != null; - const isMiscMatch = value.misc.match(this.filterRegexp) != null; - const isFilterSet = this.filter!=""; - const hasPersonOrProject = value.person!='' || value.project!=''; - const isPeopleActionNote = value.isDiscussWithNote || value.isWaitingForNote || value.isPromisedToNote; - if (!isFilterSet || isPersonMatch || isProjectMatch || isMiscMatch) { - const isToday = (date: Date) => { - let today = new Date(); - return ( - date.getDate() == today.getDate() && - date.getMonth() == today.getMonth() && - date.getFullYear() == today.getFullYear() - ); - }; - - const isBeforeToday = (date: Date) => { - let today = (new Date()) - today.setHours(0, 0, 0, 0); - return date < today; - }; - - const isAgingNote = value.actionDate && isBeforeToday(value.actionDate); - const isTodayNote = value.actionDate && isToday(value.actionDate); - const isScheduledNote = !value.isSomedayMaybeNote && value.actionDate && !isTodayNote && !isAgingNote; - - switch (this.state.activePane) { - case TodoItemViewPane.Inbox: - return !value.isSomedayMaybeNote && !isTodayNote && !isScheduledNote && !isAgingNote && !(isPeopleActionNote && hasPersonOrProject); - case TodoItemViewPane.Scheduled: - return isScheduledNote; - case TodoItemViewPane.Someday: - return value.isSomedayMaybeNote; - case TodoItemViewPane.Today: - return isTodayNote; - case TodoItemViewPane.Aging: - return isAgingNote; - case TodoItemViewPane.Stakeholder: - return hasPersonOrProject && isPeopleActionNote; - } - } else return false; - } - - private toggleTodo(todo: TodoItem): void { - this.props.toggleTodo(todo, TodoItemStatus.toggleStatus(todo.status)); - } - - private openFile(todo: TodoItem): void { - this.props.openFile(todo.sourceFilePath); - } -} +import { ItemView, MarkdownRenderer, WorkspaceLeaf } from 'obsidian'; +import { VIEW_TYPE_TODO } from '../constants'; +import { TodoItem, TodoItemStatus } from '../model/TodoItem'; +import { RenderIcon, Icon } from '../ui/icons'; + +enum TodoItemViewPane { + Aging, + Today, + Scheduled, + Inbox, + Someday, + Stakeholder, +} + +enum TodoSortStates { + None = 0, + DateAsc = 1, + DateDesc = 2, + StakeholderAsc = 3, + StakeholderDesc = 4, + ProjectAsc = 5, + ProjectDesc = 6, + MiscAsc = 7, + MiscDesc = 8, + FullTextAsc = 9, + FullTextDesc = 10, +} + +export interface TodoItemViewProps { + todos: TodoItem[]; + openFile: (filePath: string) => void; + toggleTodo: (todo: TodoItem, newStatus: TodoItemStatus) => void; + isInboxVisible: boolean; + isAgingVisible: boolean; + isTodayVisible: boolean; + isScheduledVisible: boolean; + isStakeholderVisible: boolean; + isSomedayVisible: boolean; + inboxTooltip: string; + agingTooltip: string; + todayTooltip: string; + scheduledTooltip: string; + stakeholderTooltip: string; + somedayTooltip: string; +} + +interface TodoItemViewState { + activePane: TodoItemViewPane; +} + +interface TodoSortState { + state: TodoSortStates, +} + +export class TodoItemView extends ItemView { + private props: TodoItemViewProps; + private state: TodoItemViewState; + private sortState: TodoSortState; + private filter: string; + private filterRegexp: RegExp; + private sortStateCount; + + constructor(leaf: WorkspaceLeaf, props: TodoItemViewProps) { + //debugger; + super(leaf); + this.props = props; + this.state = { + activePane: TodoItemViewPane.Today, + }; + this.sortState = { + state: TodoSortStates.None, + }; + this.sortStateCount = Object.values(TodoSortStates).length/2; + this.filter = ''; + } + + getViewType(): string { + return VIEW_TYPE_TODO; + } + + getDisplayText(): string { + return 'Obsidian GTD'; + } + + getIcon(): string { + return 'checkmark'; + } + + onClose(): Promise { + return Promise.resolve(); + } + + public setDisplayProps(props: TodoItemViewProps) { + this.props.isInboxVisible = props.isInboxVisible; + this.props.isAgingVisible = props.isAgingVisible; + this.props.isTodayVisible = props.isTodayVisible; + this.props.isScheduledVisible = props.isScheduledVisible; + this.props.isStakeholderVisible = props.isStakeholderVisible; + this.props.isSomedayVisible = props.isSomedayVisible; + this.props.inboxTooltip = props.inboxTooltip; + this.props.agingTooltip = props.agingTooltip; + this.props.todayTooltip = props.todayTooltip; + this.props.scheduledTooltip = props.scheduledTooltip; + this.props.stakeholderTooltip = props.stakeholderTooltip; + this.props.somedayTooltip = props.somedayTooltip; + this.render(); + } + + public setProps(setter: (currentProps: TodoItemViewProps) => TodoItemViewProps): void { + this.props = setter(this.props); + this.render(); + } + + private setViewState(newState: TodoItemViewState) { + this.state = newState; + if(newState.activePane == TodoItemViewPane.Aging || newState.activePane == TodoItemViewPane.Scheduled || newState.activePane == TodoItemViewPane.Today) + this.sortState = {state: TodoSortStates.DateAsc}; + else if (newState.activePane == TodoItemViewPane.Stakeholder) + this.sortState = {state: TodoSortStates.StakeholderAsc}; + else + this.sortState = {state: TodoSortStates.FullTextAsc}; + this.render(); + } + + private setSortState(newState: TodoSortState) { + this.sortState = newState; + this.render(); + } + + private setFilter(filter: string) { + this.filter = filter; + this.filterRegexp = new RegExp(filter,'i'); + this.renderViewItemsOnly(); + } + + private renderViewItemsOnly():void { + const container = this.containerEl.children[1].children[0]; + container.children[2].remove(); + container.createDiv('todo-item-view-items', (el) => { + this.renderItems(el); + }); + } + + private render(): void { + const container = this.containerEl.children[1]; + container.empty(); + container.createDiv('todo-item-view-container', (el) => { + el.createDiv('todo-item-view-search', (el) => { + this.renderSearch(el); + }); + el.createDiv('todo-item-view-toolbar', (el) => { + this.renderToolBar(el); + }); + el.createDiv('todo-item-view-items', (el) => { + this.renderItems(el); + }); + }); + } + + private renderSearch(container: HTMLDivElement) { + const activeClass = () => { + if (this.sortState.state == TodoSortStates.None) + return ' none'; + else + return ' active'; + }; + + const sortLabel = (sortState:TodoSortStates) => { + switch (sortState) { + case TodoSortStates.None: + return 'Sort by: none'; + case TodoSortStates.DateDesc: + return 'Sort by: Action Date Descending'; + case TodoSortStates.DateAsc: + return 'Sort by: Action Date Ascending'; + case TodoSortStates.StakeholderDesc: + return 'Sort by: Person Descending'; + case TodoSortStates.StakeholderAsc: + return 'Sort by: Person Ascending'; + case TodoSortStates.ProjectDesc: + return 'Sort by: Project Descending'; + case TodoSortStates.ProjectAsc: + return 'Sort by: Project Ascending'; + case TodoSortStates.MiscDesc: + return 'Sort by: Misc-snippet Descending'; + case TodoSortStates.MiscAsc: + return 'Sort by: Misc-snippet Ascending'; + case TodoSortStates.FullTextDesc: + return 'Sort by: Full Text Descending'; + case TodoSortStates.FullTextAsc: + return 'Sort by: Full Text Ascending'; + } + } + + container.createEl('input', {value: this.filter}, (el) => { + el.addClass('todo-filter-input'); + el.setAttribute('placeholder','proj/person RexExp filter, case insensitive'); + el.onkeyup = (e) => { + this.setFilter((e.target).value); + }; + }); + + container.createDiv(`todo-item-view-sort${activeClass()}`, (el) => { + el.appendChild(RenderIcon(Icon.Sort, sortLabel(this.sortState.state))); + el.onClickEvent((e) => { + const nextSortState = (this.sortState.state + 1) % this.sortStateCount; + this.setSortState({state: nextSortState}); + //(e.target).setAttribute('aria-label',sortLabel(nextSortState)); + }); + }); + } + + private renderToolBar(container: HTMLDivElement) { + const activeClass = (pane: TodoItemViewPane) => { + return pane === this.state.activePane ? ' active' : ''; + }; + + const setActivePane = (pane: TodoItemViewPane) => { + const newState = { + ...this.state, + activePane: pane, + }; + this.setViewState(newState); + }; + + if (this.props.isInboxVisible) + container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Inbox)}`, (el) => { + el.appendChild(RenderIcon(Icon.Inbox, this.props.inboxTooltip)); + el.onClickEvent(() => setActivePane(TodoItemViewPane.Inbox)); + }); + + if (this.props.isAgingVisible) + container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Aging)}`, (el) => { + el.appendChild(RenderIcon(Icon.Aging, this.props.agingTooltip)); + el.onClickEvent(() => setActivePane(TodoItemViewPane.Aging)); + }); + + if (this.props.isTodayVisible) + container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Today)}`, (el) => { + el.appendChild(RenderIcon(Icon.Today, this.props.todayTooltip)); + el.onClickEvent(() => setActivePane(TodoItemViewPane.Today)); + }); + + if (this.props.isScheduledVisible) + container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Scheduled)}`, (el) => { + el.appendChild(RenderIcon(Icon.Scheduled, this.props.scheduledTooltip)); + el.onClickEvent(() => setActivePane(TodoItemViewPane.Scheduled)); + }); + + if (this.props.isStakeholderVisible) + container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Stakeholder)}`, (el) => { + el.appendChild(RenderIcon(Icon.Stakeholder, this.props.stakeholderTooltip)); + el.onClickEvent(() => setActivePane(TodoItemViewPane.Stakeholder)); + }); + + if (this.props.isSomedayVisible) + container.createDiv(`todo-item-view-toolbar-item${activeClass(TodoItemViewPane.Someday)}`, (el) => { + el.appendChild(RenderIcon(Icon.Someday, this.props.somedayTooltip)); + el.onClickEvent(() => setActivePane(TodoItemViewPane.Someday)); + }); + } + + private renderItems(container: HTMLDivElement) { + const sortView = (a: TodoItem, b: TodoItem) => { + return this.sortView(a,b); + }; + const todosToRender = this.props.todos + .filter(this.filterForState, this); + const sortedTodos = todosToRender.sort(sortView); + sortedTodos + .forEach((todo,index) => { + if(index>0) { + if( (todo.isWaitingForNote && todosToRender[index-1].isDiscussWithNote) || + (todo.isPromisedToNote && + (todosToRender[index-1].isWaitingForNote || todosToRender[index-1].isDiscussWithNote)) || + (!todo.isPromisedToNote && !todo.isWaitingForNote && !todo.isDiscussWithNote && + (todosToRender[index-1].isWaitingForNote || todosToRender[index-1].isDiscussWithNote || todosToRender[index-1].isPromisedToNote)) ) { + container.createEl('hr', {} ,(el) => { + el.addClass('todo-item-view-divider'); + }); + } + } + container.createDiv('todo-item-view-item', (el) => { + el.createDiv('todo-item-view-item-checkbox', (el) => { + el.createEl('input', { type: 'checkbox' }, (el) => { + el.checked = todo.status === TodoItemStatus.Done; + el.onClickEvent(() => { + this.toggleTodo(todo); + }); + }); + }); + el.createDiv('todo-item-view-item-description', (el) => { + MarkdownRenderer.renderMarkdown(todo.description, el, todo.sourceFilePath, this); + }); + el.createDiv('todo-item-view-item-link', (el) => { + el.appendChild(RenderIcon(Icon.Reveal, 'Open file')); + el.onClickEvent(() => { + this.openFile(todo); + }); + }); + }); + }); + } + + private sortView(a: TodoItem, b: TodoItem) { + let sortResult = 0; + switch (this.sortState.state) { + case TodoSortStates.None: + sortResult = 0;break; + case TodoSortStates.DateAsc: + sortResult = a.actionDate < b.actionDate ? -1 : a.actionDate > b.actionDate ? 1 : 0;break; + case TodoSortStates.DateDesc: + sortResult = a.actionDate > b.actionDate ? -1 : a.actionDate < b.actionDate ? 1 : 0;break; + case TodoSortStates.StakeholderAsc: + sortResult = a.person < b.person ? -1 : a.person > b.person ? 1 : 0;break; + case TodoSortStates.StakeholderDesc: + sortResult = a.person > b.person ? -1 : a.person < b.person ? 1 : 0;break; + case TodoSortStates.ProjectAsc: + sortResult = a.project < b.project ? -1 : a.project > b.project ? 1 : 0;break; + case TodoSortStates.ProjectDesc: + sortResult = a.project > b.project ? -1 : a.project < b.project ? 1 : 0;break; + case TodoSortStates.MiscAsc: + sortResult = a.misc < b.misc ? -1 : a.misc > b.misc ? 1 : 0;break; + case TodoSortStates.MiscDesc: + sortResult = a.misc > b.misc ? -1 : a.misc < b.misc ? 1 : 0;break; + case TodoSortStates.FullTextAsc: + sortResult = a.description.toLowerCase() < b.description.toLowerCase() ? -1 : a.description.toLowerCase() > b.description.toLowerCase() ? 1 : 0;break; + case TodoSortStates.FullTextDesc: + sortResult = a.description.toLowerCase() > b.description.toLowerCase() ? -1 : a.description.toLowerCase() < b.description.toLowerCase() ? 1 : 0;break; + } + + if (this.state.activePane == TodoItemViewPane.Stakeholder) { + if (a.isDiscussWithNote && !b.isDiscussWithNote) { + return -1; + } + if (a.isWaitingForNote && !b.isDiscussWithNote && !b.isWaitingForNote) { + return -1; + } + if (a.isPromisedToNote && !b.isDiscussWithNote && !b.isWaitingForNote) { + return -1; + } + if (b.isDiscussWithNote && !a.isDiscussWithNote) { + return 1; + } + if (b.isWaitingForNote && !a.isDiscussWithNote && !a.isWaitingForNote) { + return 1; + } + if (b.isPromisedToNote && !a.isDiscussWithNote && !a.isWaitingForNote) { + return 1; + } + return sortResult; + } + return sortResult; + } + + private filterForState(value: TodoItem, _index: number, _array: TodoItem[]): boolean { + const isPersonMatch = value.person.match(this.filterRegexp) != null; + const isProjectMatch = value.project.match(this.filterRegexp) != null; + const isMiscMatch = value.misc.match(this.filterRegexp) != null; + const isFilterSet = this.filter!=""; + const hasPersonOrProject = value.person!='' || value.project!=''; + const isPeopleActionNote = value.isDiscussWithNote || value.isWaitingForNote || value.isPromisedToNote; + if (!isFilterSet || isPersonMatch || isProjectMatch || isMiscMatch) { + const isToday = (date: Date) => { + let today = new Date(); + return ( + date.getDate() == today.getDate() && + date.getMonth() == today.getMonth() && + date.getFullYear() == today.getFullYear() + ); + }; + + const isBeforeToday = (date: Date) => { + let today = (new Date()) + today.setHours(0, 0, 0, 0); + return date < today; + }; + + const isAgingNote = value.actionDate && isBeforeToday(value.actionDate); + const isTodayNote = value.actionDate && isToday(value.actionDate); + const isScheduledNote = !value.isSomedayMaybeNote && value.actionDate && !isTodayNote && !isAgingNote; + + switch (this.state.activePane) { + case TodoItemViewPane.Inbox: + return !value.isSomedayMaybeNote && !isTodayNote && !isScheduledNote && !isAgingNote && !(isPeopleActionNote && hasPersonOrProject); + case TodoItemViewPane.Scheduled: + return isScheduledNote; + case TodoItemViewPane.Someday: + return value.isSomedayMaybeNote; + case TodoItemViewPane.Today: + return isTodayNote; + case TodoItemViewPane.Aging: + return isAgingNote; + case TodoItemViewPane.Stakeholder: + return hasPersonOrProject && isPeopleActionNote; + } + } else return false; + } + + private toggleTodo(todo: TodoItem): void { + this.props.toggleTodo(todo, TodoItemStatus.toggleStatus(todo.status)); + } + + private openFile(todo: TodoItem): void { + this.props.openFile(todo.sourceFilePath); + } +}