From 6719c04c487097d931e7f9cb8e4699e7c7f1c220 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Tue, 24 Oct 2023 14:52:46 -0700 Subject: [PATCH 01/20] =?UTF-8?q?=F0=9F=90=9B=20fixing=20duration=20and=20?= =?UTF-8?q?due=20display=20bugs?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/googleCalendarAPI/calendarAPI.ts | 3 +- .../syncSettings/googleCalendarSettings.ts | 2 +- src/taskModule/task.ts | 2 +- src/ui/Due.svelte | 60 ++++++++-------- src/ui/Duration.svelte | 69 +++++++++++++------ src/ui/TaskCard.svelte | 12 +--- 6 files changed, 87 insertions(+), 61 deletions(-) diff --git a/src/api/googleCalendarAPI/calendarAPI.ts b/src/api/googleCalendarAPI/calendarAPI.ts index c067c1e..6b2bc4a 100644 --- a/src/api/googleCalendarAPI/calendarAPI.ts +++ b/src/api/googleCalendarAPI/calendarAPI.ts @@ -128,7 +128,8 @@ export class GoogleCalendarAPI { constructStartAndEnd(task: Partial): {start: GoogleEventTimePoint, end: GoogleEventTimePoint} { if (!task.due) { - throw new Error("Task must have a due date."); + logger.error(`Task has no due date: ${JSON.stringify(task)}`); + return {start: {}, end: {}} } const due = task.due; diff --git a/src/settings/syncSettings/googleCalendarSettings.ts b/src/settings/syncSettings/googleCalendarSettings.ts index 55b3b79..14ffb87 100644 --- a/src/settings/syncSettings/googleCalendarSettings.ts +++ b/src/settings/syncSettings/googleCalendarSettings.ts @@ -45,7 +45,7 @@ export async function googleCalendarSyncSettings( 'a', { text: 'the documentation', - href: 'https://github.com/terryli710/Obsidian-TaskCard/blob/feature/google-calendar-api/docs/google-calendar-sync-setup.md', // TODO: correct this link. + href: 'https://github.com/terryli710/Obsidian-TaskCard/blob/feature/google-calendar-api/docs/google-calendar-sync-setup.md', } ); frag.appendText(' for more details.'); diff --git a/src/taskModule/task.ts b/src/taskModule/task.ts index be87be0..c85dace 100644 --- a/src/taskModule/task.ts +++ b/src/taskModule/task.ts @@ -136,7 +136,7 @@ export class ObsidianTask implements TaskProperties { hasDue(): boolean { if (!this.due) return false; // return if the due string is not empty - return !!this.due.string; + return !!this.due.string.trim(); } hasDuration(): boolean { diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index c0c0583..8ef4d4b 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -12,6 +12,8 @@ export let taskSyncManager: ObsidianTaskSyncManager; export let plugin: TaskCardPlugin; export let params: TaskDisplayParams; + export let displayDue: boolean; + let due: DueDate | null; let dueString: string; let duration: Duration | null; @@ -23,9 +25,6 @@ updateDueDisplay(); - // TODO: bug: when there's no due, added an wrong due string; - // TODO: delete due string would result in due error and using original string; - async function toggleEditMode(event: KeyboardEvent | MouseEvent) { if (taskSyncManager.taskCardStatus.dueStatus === 'done') { @@ -53,32 +52,37 @@ } function finishDueEditing(event: KeyboardEvent | MouseEvent) { - if (event instanceof MouseEvent) { + if (event instanceof MouseEvent) { return; - } - if (event.key === 'Enter') { - // event.preventDefault(); - // taskSyncManager.setTaskCardStatus('dueStatus', 'done'); - taskSyncManager.taskCardStatus.dueStatus = 'done'; - let newDue: DueDate | null; - try { - newDue = plugin.taskParser.parseDue(dueString); - } catch (e) { + } + + if (event.key === 'Enter' || event.key === 'Escape') { + taskSyncManager.taskCardStatus.dueStatus = 'done'; + if (event.key === 'Escape') { + updateDueDisplay(); + return; + } + } + + if (dueString.trim() === '') { + due = null; + } else { + try { + let newDue = plugin.taskParser.parseDue(dueString); + if (newDue) { + due = newDue; + } else { + new Notice(`[TaskCard] Invalid due date: ${dueString}`); + dueString = due ? due.string : ''; + } + } catch (e) { logger.error(e); - } - if (!newDue) { - new Notice(`[TaskCard] Invalid due date: ${dueString}`); dueString = due ? due.string : ''; - } else { - due = newDue; - } - taskSyncManager.updateObsidianTaskAttribute('due', due); - updateDueDisplay(); - } else if (event.key === 'Escape') { - event.preventDefault(); - // taskSyncManager.setTaskCardStatus('dueStatus', 'done'); - taskSyncManager.taskCardStatus.dueStatus = 'done'; - } + } + } + + taskSyncManager.updateObsidianTaskAttribute('due', due); + updateDueDisplay(); } function updateDueDisplay(): string { @@ -120,9 +124,7 @@ node.style.width = Math.min(Math.max(newWidth, minWidth), maxWidth) + 'px'; } - // let isEditingDue = taskSyncManager.taskCardStatus.dueStatus === 'editing'; - let displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.taskCardStatus.dueStatus === 'editing'; - // let displayDuration = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; + $: displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.taskCardStatus.dueStatus === 'editing'; diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index 7d1c19b..a173aa7 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -9,14 +9,12 @@ import { Notice } from "obsidian"; import History from "../components/icons/History.svelte"; - export let taskSyncManager: ObsidianTaskSyncManager; - // export let plugin: TaskCardPlugin; export let params: TaskDisplayParams; + export let displayDuration: boolean; let duration: Duration | null; duration = taskSyncManager.obsidianTask.hasDuration() ? taskSyncManager.obsidianTask.duration : null; - // TODO: bug: when there's no duration, added zero duration or wrong duration string; function customDurationHumanizer(duration: Duration) { if (duration.hours === 0) { @@ -68,30 +66,61 @@ } } taskSyncManager.taskCardStatus.durationStatus = 'editing'; - durationInputString = duration ? `${pad(duration.hours, 2)}:${pad(duration.minutes, 2)}` : '00:00'; + durationInputString = duration ? `${pad(duration.hours, 2)}:${pad(duration.minutes, 2)}` : '01:00'; origDurationInputString = durationInputString; await tick(); focusAndSelect(durationInputElement); } function finishDurationEditing(event: KeyboardEvent | MouseEvent) { - if (event instanceof MouseEvent) { - return; - } + if (event instanceof MouseEvent) { + return; + } - if (event.key === 'Enter' && durationInputString !== origDurationInputString) { - const [hours, minutes] = durationInputString.split(":").map(Number); - duration = { hours, minutes }; - taskSyncManager.updateObsidianTaskAttribute('duration', duration); - origDurationInputString = durationInputString; - } + // Handle the Escape key, as the logic is simpler + if (event.key === 'Escape') { + event.preventDefault(); + taskSyncManager.taskCardStatus.durationStatus = 'done'; + return; + } - if (event.key === 'Escape' || event.key === 'Enter') { - event.preventDefault(); - taskSyncManager.taskCardStatus.durationStatus = 'done'; - } + // Main logic for the Enter key + if (event.key === 'Enter') { + logger.debug(`duration finished: ${durationInputString}`); + + // Early exit for empty string or 00:00 duration + if (durationInputString.trim() === '' || isValidZeroDuration(durationInputString)) { + duration = null; + } else { + const parsedDuration = parseDurationInput(durationInputString); + if (parsedDuration) { + duration = parsedDuration; + } else { + new Notice(`[TaskCard] Invalid duration format: ${durationInputString}`); + } + } - } + taskSyncManager.updateObsidianTaskAttribute('duration', duration); + origDurationInputString = durationInputString; + + updateDurationDisplay(); + event.preventDefault(); + taskSyncManager.taskCardStatus.durationStatus = 'done'; + } +} + +function isValidZeroDuration(input: string): boolean { + const [hours, minutes] = input.split(":").map(Number); + return hours === 0 && minutes === 0; +} + +function parseDurationInput(input: string): { hours: number, minutes: number } | null { + const [hours, minutes] = input.split(":").map(Number); + if (!isNaN(hours) && !isNaN(minutes)) { + return { hours, minutes }; + } + return null; +} // Action function to focus and select the input content @@ -103,7 +132,7 @@ } let displayDue: boolean = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'; - let displayDuration: boolean = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; + $: displayDuration = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; @@ -129,7 +158,7 @@ bind:value={durationInputString} bind:this={durationInputElement} class="task-card-duration" - placeholder="00:00" + placeholder="hh:mm" /> {:else}
diff --git a/src/ui/TaskCard.svelte b/src/ui/TaskCard.svelte index 9d603b0..ed43dc7 100644 --- a/src/ui/TaskCard.svelte +++ b/src/ui/TaskCard.svelte @@ -230,9 +230,7 @@ {#if descriptionProgress[1] * descriptionProgress[0] > 0 && !task.completed } {/if} - {#if taskSyncManager.obsidianTask.hasDue()} - - {/if} +
@@ -263,12 +261,8 @@
- {#if displayDue} - - {/if} - {#if displayDuration} - - {/if} + + {#if displayDue || displayDuration}
{/if} From e3b3f329ce30b6178f7fb9a96d01d1a7166063cc Mon Sep 17 00:00:00 2001 From: terryli710 Date: Wed, 25 Oct 2023 10:12:44 -0700 Subject: [PATCH 02/20] =?UTF-8?q?=F0=9F=90=9B=20bug=20fixes:=20make=20sure?= =?UTF-8?q?=20plugin=20function=20wo=20ggl=20api.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/externalAPIManager.ts | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/src/api/externalAPIManager.ts b/src/api/externalAPIManager.ts index ece03db..948f159 100644 --- a/src/api/externalAPIManager.ts +++ b/src/api/externalAPIManager.ts @@ -7,7 +7,7 @@ import { GoogleCalendarAPI } from "./googleCalendarAPI/calendarAPI"; export interface SyncMappings { - googleSyncSetting: { + googleSyncSetting?: { id: string; } } @@ -67,21 +67,29 @@ export class ExternalAPIManager { async notifyTaskCreations(event: TaskChangeEvent): Promise { if (event.type !== TaskChangeType.ADD) return; - const id = await this.googleCalendarAPI.handleLocalTaskCreation(event); const oldSyncMappings = event.currentState.metadata.syncMappings || {}; - return { ...oldSyncMappings, googleSyncSetting: { id: id } }; + let syncMappings = oldSyncMappings; + if (this.googleCalendarAPI) { + const id = await this.googleCalendarAPI.handleLocalTaskCreation(event); + syncMappings = { ...syncMappings, googleSyncSetting: { id: id } }; + } + return syncMappings; } async notifyTaskUpdates(event: TaskChangeEvent): Promise { if (event.type !== TaskChangeType.UPDATE) return; - const id = await this.googleCalendarAPI.handleLocalTaskUpdate(event); const oldSyncMappings = event.currentState.metadata.syncMappings || {}; - return { ...oldSyncMappings, googleSyncSetting: { id: id } }; + let syncMappings = oldSyncMappings; + if (this.googleCalendarAPI) { + const id = await this.googleCalendarAPI.handleLocalTaskUpdate(event); + syncMappings = { ...syncMappings, googleSyncSetting: { id: id } }; + } + return syncMappings; } notifyTaskDeletions(event: TaskChangeEvent) { if (event.type !== TaskChangeType.REMOVE) return; - this.googleCalendarAPI.handleLocalTaskDeletion(event); + if (this.googleCalendarAPI) this.googleCalendarAPI.handleLocalTaskDeletion(event); } From 1f7b3eb8c1269a3471dfeea09a090acc9b7fc331 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Wed, 25 Oct 2023 11:49:29 -0700 Subject: [PATCH 03/20] =?UTF-8?q?=F0=9F=90=9B=20fix=20description=20parsin?= =?UTF-8?q?g=20bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../description/descriptionParser.ts | 26 +++++----- src/taskModule/taskMonitor.ts | 5 +- src/taskModule/taskParser.ts | 49 +++++++++++++++++-- 3 files changed, 60 insertions(+), 20 deletions(-) diff --git a/src/taskModule/description/descriptionParser.ts b/src/taskModule/description/descriptionParser.ts index 19148f9..657228e 100644 --- a/src/taskModule/description/descriptionParser.ts +++ b/src/taskModule/description/descriptionParser.ts @@ -32,21 +32,21 @@ export class DescriptionParser { } // Parses the description from a given task element - static parseDescriptionFromTaskEl(taskElement: HTMLElement): string { - const listElements = DescriptionParser.extractListEls(taskElement); - if (listElements.length === 0) { return ""; } - let descriptionMarkdown = ""; + static parseDescriptionFromTaskEl(taskElement: HTMLElement): string { + const listElements = DescriptionParser.extractListEls(taskElement); + if (listElements.length === 0) { return ""; } + let descriptionMarkdown = ""; - for (const listEl of listElements) { - try { - descriptionMarkdown += htmlToMarkdown(listEl.outerHTML) + "\n"; - } catch (error) { - throw new Error(`Failed to convert HTML to Markdown: ${error.message}`); - } - } + for (const listEl of listElements) { + try { + descriptionMarkdown += htmlToMarkdown(listEl.outerHTML) + "\n"; + } catch (error) { + throw new Error(`Failed to convert HTML to Markdown: ${error.message}`); + } + } - return descriptionMarkdown.trim(); - } + return descriptionMarkdown.trim(); + } static progressOfDescription(description: string): [number, number] { if (!description || description.trim().length === 0) { return [0, 0]; } diff --git a/src/taskModule/taskMonitor.ts b/src/taskModule/taskMonitor.ts index 7ef0f67..a67153f 100644 --- a/src/taskModule/taskMonitor.ts +++ b/src/taskModule/taskMonitor.ts @@ -70,7 +70,7 @@ export class TaskMonitor { const taskDetails = this.detectTasksFromLines(lines); if (taskDetails.length === 0) return; for (const taskDetail of taskDetails) { - + logger.debug(`taskDetail: ${JSON.stringify(taskDetail)}`); // notify API about creation of tasks let task = this.parseTaskWithLines(taskDetail.taskMarkdown.split('\n')); const syncMetadata = await this.plugin.externalAPIManager.createTask(task); @@ -114,6 +114,7 @@ export class TaskMonitor { } return task; } else { + announceError('Failed to parse task: ' + taskMarkdown); return null; } @@ -145,8 +146,6 @@ export class TaskMonitor { endLine: lineIndex + 1 + descriptionLineCount, }); - // Skip the description lines in the next iterations - lineIndex += descriptionLineCount; } lineIndex++; } diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index 43d4458..af1e0f4 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -72,7 +72,7 @@ export class TaskParser { } } - // logger.debug(`Parsing task element: ${taskEl.outerHTML}`); + logger.debug(`Parsing task element: ${taskEl.outerHTML}`); const task = new ObsidianTask(); const attributes = parseAttributes.bind(this)(); @@ -184,10 +184,51 @@ export class TaskParser { } }; - // if taskMarkdown is multi-line, split it + // Utility function to convert leading tabs in a line to spaces + const convertLeadingTabsToSpaces = (line: string): string => { + let result = ""; + let index = 0; + + while (index < line.length) { + if (line[index] === '\t') { + result += " "; // Replace tab with 4 spaces + } else if (line[index] === ' ') { + result += line[index]; + } else { + // Once we encounter a non-space, non-tab character, break + break; + } + index++; + } + + // Append the remainder of the line + result += line.slice(index); + return result; + }; + + // Utility function to check if all lines start with a space + const allLinesStartWithSpace = (lines: string[]): boolean => { + return lines.every(line => line.startsWith(" ")); + }; + + // Utility function to remove the leading space from all lines + const removeLeadingSpace = (lines: string[]): string[] => { + return lines.map(line => line.startsWith(" ") ? line.slice(1) : line); + }; + if (taskMarkdown.includes('\n')) { - const lines = taskMarkdown.split('\n'); - task.description = lines.slice(1).join('\n'); // From the second line to the last line, joined by '\n' + // process multi-line task - has description + let lines = taskMarkdown.split('\n'); + let descLines = lines.slice(1); + // Convert tabs to spaces + descLines = descLines.map(convertLeadingTabsToSpaces); + + // Iteratively remove indentation + while (allLinesStartWithSpace(descLines)) { + descLines = removeLeadingSpace(descLines); + } + logger.debug(`Multi-line task: ${descLines.join('\n')}`); + task.description = descLines.join('\n'); taskMarkdown = lines[0]; // The first line } From b0edbfa57357c15b475a6b3aaffab9a6c146427c Mon Sep 17 00:00:00 2001 From: terryli710 Date: Wed, 25 Oct 2023 20:48:34 -0700 Subject: [PATCH 04/20] =?UTF-8?q?=F0=9F=90=9B=20fix=20bug=20when=20creatin?= =?UTF-8?q?g=20due?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/Due.svelte | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index 8ef4d4b..75adf17 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -62,27 +62,27 @@ updateDueDisplay(); return; } - } - if (dueString.trim() === '') { - due = null; - } else { - try { - let newDue = plugin.taskParser.parseDue(dueString); - if (newDue) { - due = newDue; - } else { - new Notice(`[TaskCard] Invalid due date: ${dueString}`); + if (dueString.trim() === '') { + due = null; + } else { + try { + let newDue = plugin.taskParser.parseDue(dueString); + if (newDue) { + due = newDue; + } else { + new Notice(`[TaskCard] Invalid due date: ${dueString}`); + dueString = due ? due.string : ''; + } + } catch (e) { + logger.error(e); dueString = due ? due.string : ''; } - } catch (e) { - logger.error(e); - dueString = due ? due.string : ''; } - } - taskSyncManager.updateObsidianTaskAttribute('due', due); - updateDueDisplay(); + taskSyncManager.updateObsidianTaskAttribute('due', due); + updateDueDisplay(); + } } function updateDueDisplay(): string { From 61a434cb36dcde89c225d7ae15cd0960335d86ae Mon Sep 17 00:00:00 2001 From: terryli710 Date: Thu, 26 Oct 2023 23:38:40 -0700 Subject: [PATCH 05/20] =?UTF-8?q?=E2=9C=A8=20advanced=20due=20date=20sugge?= =?UTF-8?q?ster?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoSuggestions/Suggester.ts | 133 ++++++++++++++++++++++++------- src/settings.ts | 4 +- src/ui/Due.svelte | 78 ++++++++++++++++-- 3 files changed, 178 insertions(+), 37 deletions(-) diff --git a/src/autoSuggestions/Suggester.ts b/src/autoSuggestions/Suggester.ts index b690e40..68df4ce 100644 --- a/src/autoSuggestions/Suggester.ts +++ b/src/autoSuggestions/Suggester.ts @@ -12,8 +12,9 @@ export class AttributeSuggester { private projects: Project[]; private nonInputtableAttributes: string[]; private inputtableAttributes: string[]; + private maxNumberOfSuggestions: number; - constructor(settingsStore: typeof SettingStore) { + constructor(settingsStore: typeof SettingStore, maxNumberOfSuggestions=10) { // Subscribe to the settings store settingsStore.subscribe((settings) => { this.startingNotation = settings.parsingSettings.markdownStartingNotation; @@ -31,19 +32,16 @@ export class AttributeSuggester { 'description', 'metadata' ]; - - // // Filter out the non-inputtable attributes - // this.inputtableAttributes = Object.keys(new ObsidianTask()).filter( - // (attr) => !this.nonInputtableAttributes.includes(attr) - // ); - - this.inputtableAttributes = [ - 'due', - 'duration', - 'priority', - 'project', - ] }); + + this.inputtableAttributes = [ + 'due', + 'duration', + 'priority', + 'project', + ] + + this.maxNumberOfSuggestions = maxNumberOfSuggestions; } buildSuggestions(lineText: string, cursorPos: number): SuggestInformation[] { @@ -63,7 +61,7 @@ export class AttributeSuggester { suggestions = suggestions.concat( this.getProjectSuggestions(lineText, cursorPos) ); - return suggestions; + return suggestions.slice(0, this.maxNumberOfSuggestions); } getAttributeSuggestions( @@ -138,19 +136,19 @@ export class AttributeSuggester { return suggestions; } + getDueSuggestions(lineText: string, cursorPos: number): SuggestInformation[] { let suggestions: SuggestInformation[] = []; - - // Modify regex to capture the due date query + const dueRegexText = `${escapeRegExp(this.startingNotation)}\\s?due:(.*?)${escapeRegExp(this.endingNotation)}`; const dueRegex = new RegExp(dueRegexText, 'g'); const dueMatch = matchByPositionAndGroup(lineText, dueRegex, cursorPos, 1); - if (!dueMatch) return suggestions; // No match - - // Get the due date query from the captured group + if (!dueMatch) return suggestions; + const dueQuery = (dueMatch[1] || '').trim(); - - const dueStringSelections = [ + const dueQueryLower = dueQuery.toLowerCase(); + + const level1Suggestions = [ 'today', 'tomorrow', 'Sunday', @@ -163,21 +161,64 @@ export class AttributeSuggester { 'next week', 'next month', 'next year' - ]; - - // Use the dueQuery to filter the suggestions - const filteredDueStrings = dueStringSelections.filter((dueString) => - dueString.toLowerCase().startsWith(dueQuery.toLowerCase()) - ); + ] + const level2Suggestions = [ + 'at 9', + 'at 10', + 'at 11', + 'at 12', + 'at 1', + 'at 2', + 'at 3', + 'at 4', + 'at 5', + 'at 6', + 'at 7', + 'at 8' + ] + + const level3Suggestions = [ + 'am', + 'pm', + ':15', + ':30', + ':45', + ] + + const level4Suggestions = [ + 'am', + 'pm', + ] + + const suggestionLevels = [level1Suggestions, level2Suggestions, level3Suggestions, level4Suggestions]; + const levelInfo = determineLevel(dueQueryLower, suggestionLevels); + const filteredSuggestions = levelInfo.filteredSuggestions; + + suggestions = filteredSuggestions.map((filteredSuggestion) => { + // Construct the replaceText with the dueQuery, the current suggestion, and a space if not the last level + const isLastLevel = suggestionLevels[suggestionLevels.length-1].contains(filteredSuggestion.trim()); + if (!filteredSuggestion.startsWith(":")) { + filteredSuggestion = ` ${filteredSuggestion}`; + } + if (!isLastLevel) { + filteredSuggestion = `${filteredSuggestion} `; + } + const replaceText = `${this.startingNotation}due: ${dueQuery.trim()}${filteredSuggestion}${this.endingNotation}`; + + // Set the cursor position to be right after the current suggestion + let cursorPosition: number; + if (isLastLevel) { + cursorPosition = dueMatch.index + `${this.startingNotation}due: ${dueQuery.trim()}${filteredSuggestion}${this.endingNotation}`.length; + } else { + cursorPosition = dueMatch.index + `${this.startingNotation}due: ${dueQuery.trim()}${filteredSuggestion}`.length; + } - suggestions = filteredDueStrings.map((dueString) => { - const replaceText = `${this.startingNotation}due: ${dueString}${this.endingNotation} `; return { - displayText: dueString, + displayText: filteredSuggestion, replaceText: replaceText, replaceFrom: dueMatch.index, replaceTo: dueMatch.index + dueMatch[0].length, - cursorPosition: dueMatch.index + replaceText.length + cursorPosition: cursorPosition }; }); @@ -347,3 +388,33 @@ export function adjustEndPosition( } return 0; } + + +interface LevelInfo { + level: number; + remainingQuery: string; + filteredSuggestions: string[]; +} + +function determineLevel(dueQuery: string, suggestionLevels: string[][]): LevelInfo { + let currentQuery = dueQuery.trim(); + let currentLevel = 0; + let filteredSuggestions: string[] = []; + + for (const suggestions of suggestionLevels) { + const matchingSuggestion = suggestions.find(suggestion => currentQuery.startsWith(suggestion)); + if (matchingSuggestion) { + currentLevel++; + currentQuery = currentQuery.slice(matchingSuggestion.length).trim(); + } else { + filteredSuggestions = suggestions.filter(suggestion => suggestion.startsWith(currentQuery)); + break; + } + } + + return { + level: currentLevel, + remainingQuery: currentQuery, + filteredSuggestions: filteredSuggestions, + }; +} \ No newline at end of file diff --git a/src/settings.ts b/src/settings.ts index 77acfad..8f44049 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -23,6 +23,7 @@ export interface TaskCardSettings { }; displaySettings: { defaultMode: string; + upcomingMinutes: number; }; userMetadata: { projects: any; @@ -49,7 +50,8 @@ export const DefaultSettings: TaskCardSettings = { blockLanguage: 'taskcard' }, displaySettings: { - defaultMode: 'single-line' + defaultMode: 'single-line', + upcomingMinutes: 15, }, userMetadata: { projects: {}, diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index 75adf17..7e14d3c 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -8,6 +8,7 @@ import { TaskDisplayParams, TaskDisplayMode } from "../renderer/postProcessor"; import { Notice } from "obsidian"; import CalendarClock from "../components/icons/CalendarClock.svelte"; + import moment from "moment"; export let taskSyncManager: ObsidianTaskSyncManager; export let plugin: TaskCardPlugin; @@ -25,7 +26,6 @@ updateDueDisplay(); - async function toggleEditMode(event: KeyboardEvent | MouseEvent) { if (taskSyncManager.taskCardStatus.dueStatus === 'done') { enableDueEditMode(event); @@ -124,12 +124,52 @@ node.style.width = Math.min(Math.max(newWidth, minWidth), maxWidth) + 'px'; } + + function convertDueToMoment(dueDate: DueDate): moment.Moment | null { + if (!dueDate.date) { + return null; // Ensure the date is present. + } + + let datetimeStr = dueDate.date; + if (dueDate.time) { + datetimeStr += ' ' + dueDate.time; + } + + return moment(datetimeStr); + } + $: displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.taskCardStatus.dueStatus === 'editing'; + + + // upcoming and ongoing + function isUpcoming(due: DueDate, currTime: moment.Moment, minuteGap: number = 15): boolean { + if (!due || !due.date || !due.time) { return false; } + // convert due to moment + const dueMoment = convertDueToMoment(due); + // calculate the time gap + const timeGap = moment.duration(dueMoment.diff(currTime)).asMinutes(); + // check if the time gap is within the specified range + return timeGap > 0 && timeGap < minuteGap; + } + + function isOngoing(due: DueDate, duration: Duration = undefined, currTime: moment.Moment): boolean { + if (!due || !due.date || !due.time || !duration) { return false; } + // convert due to moment + const dueMoment = convertDueToMoment(due); + // convert duration to moment + const endMoment = currTime.clone().add(duration.hours, 'hours').add(duration.minutes, 'minutes'); + // calculate if the time gap is within the specified range + return currTime.isBetween(dueMoment, endMoment); + } + + const now = moment(); + const upcoming = isUpcoming(due, now); + const ongoing = isOngoing(due, duration, now); {#if displayDue} -
- + + +
{#if taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} {:else} -
+
{dueDisplay}
@@ -180,6 +222,14 @@ padding-top: 1.5px; } + .task-card-due-prefix.ongoing { + color: var(--text-on-accent); + } + + .task-card-due-prefix.ongoing:hover { + color: var(--text-on-accent-hover); + } + .task-card-due-container { align-items: center; display: flex; @@ -189,6 +239,10 @@ border: var(--border-width) solid var(--text-accent); } + .task-card-due-container.ongoing { + background-color: var(--interactive-accent); + } + .task-card-due-container.mode-multi-line { margin-top: 2px; } @@ -213,6 +267,15 @@ line-height: 1; } + .task-card-due.upcoming { + font-style: italic; + text-decoration: underline; + } + + .task-card-due.ongoing { + color: var(--text-on-accent); + } + /* This selector ensures that the cursor only changes to a hand pointer when the div is not empty */ .task-card-due-container.mode-multi-line:not(:empty):hover { background-color: var(--background-modifier-hover); @@ -220,6 +283,11 @@ cursor: pointer; } + .task-card-due-container.mode-multi-line.ongoing:not(:empty):hover { + background-color: var(--interactive-accent-hover); + color: var(--text-accent-hover); + } + input.task-card-due { box-sizing: border-box; border: none; @@ -229,7 +297,7 @@ padding-right: var(--tag-padding-x); width: auto; height: auto; - color: var(--text-accent); + /* color: var(--text-accent); */ white-space: nowrap; line-height: 1; font-family: var(--font-text); From 6b4d6269e07c89d5082bd9f1ba08667ddcd29463 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Thu, 26 Oct 2023 23:59:00 -0700 Subject: [PATCH 06/20] =?UTF-8?q?=E2=9C=A8=20new=20display=20setting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/settings.ts | 48 +++++++------- src/settings/displaySettings.ts | 62 +++++++++++++++++++ .../syncSettings/googleCalendarSettings.ts | 4 +- 3 files changed, 91 insertions(+), 23 deletions(-) create mode 100644 src/settings/displaySettings.ts diff --git a/src/settings.ts b/src/settings.ts index 8f44049..6b86dd3 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -6,6 +6,7 @@ import { Project } from './taskModule/project'; import { logger } from './utils/log'; import { LabelModule } from './taskModule/labels/index'; import { GoogleSyncSetting, googleCalendarSyncSettings } from './settings/syncSettings/googleCalendarSettings'; +import { cardDisplaySettings } from './settings/displaySettings'; export let emptyProject: Project = { @@ -102,7 +103,12 @@ export class SettingsTab extends PluginSettingTab { this.cardParsingSettings(); // display settings this.containerEl.createEl('h2', { text: 'Display Settings' }); - this.cardDisplaySettings(); + cardDisplaySettings( + this.containerEl, + this.plugin.settings, + this.plugin.writeSettings.bind(this.plugin), + this.display.bind(this), + ); // sync settings // 1. google calendar this.containerEl.createEl('h2', { text: 'Sync Settings' }); @@ -585,26 +591,26 @@ export class SettingsTab extends PluginSettingTab { } - cardDisplaySettings() { - new Setting(this.containerEl) - .setName('Default Display Mode') - .setDesc('The default display mode when creating a new task card.') - .addDropdown((dropdown) => { - dropdown - .addOptions({ - 'single-line': 'Preview Mode', - 'multi-line': 'Detailed Mode' - }) - .setValue(this.plugin.settings.displaySettings.defaultMode) - .onChange(async (value: string) => { - await this.plugin.writeSettings( - (old) => (old.displaySettings.defaultMode = value) - ); - logger.info(`Default display mode updated: ${value}`); - new Notice(`[TaskCard] Default display mode updated: ${value}.`); - }); - }); - } + // cardDisplaySettings() { + // new Setting(this.containerEl) + // .setName('Default Display Mode') + // .setDesc('The default display mode when creating a new task card.') + // .addDropdown((dropdown) => { + // dropdown + // .addOptions({ + // 'single-line': 'Preview Mode', + // 'multi-line': 'Detailed Mode' + // }) + // .setValue(this.plugin.settings.displaySettings.defaultMode) + // .onChange(async (value: string) => { + // await this.plugin.writeSettings( + // (old) => (old.displaySettings.defaultMode = value) + // ); + // logger.info(`Default display mode updated: ${value}`); + // new Notice(`[TaskCard] Default display mode updated: ${value}.`); + // }); + // }); + // } } diff --git a/src/settings/displaySettings.ts b/src/settings/displaySettings.ts new file mode 100644 index 0000000..7b825fd --- /dev/null +++ b/src/settings/displaySettings.ts @@ -0,0 +1,62 @@ +import { Notice, Setting } from "obsidian"; +import { TaskCardSettings } from "../settings"; +import { logger } from "../utils/log"; + + + + + +export function cardDisplaySettings( + containerEl: HTMLElement, + pluginSettings: TaskCardSettings, + writeSettings: Function, + display: Function, +) { + new Setting(containerEl) + .setName('Default Display Mode') + .setDesc('The default display mode when creating a new task card.') + .addDropdown((dropdown) => { + dropdown + .addOptions({ + 'single-line': 'Preview Mode', + 'multi-line': 'Detailed Mode' + }) + .setValue(pluginSettings.displaySettings.defaultMode) + .onChange(async (value: string) => { + await writeSettings( + (old) => (old.displaySettings.defaultMode = value) + ); + logger.info(`Default display mode updated: ${value}`); + new Notice(`[TaskCard] Default display mode updated: ${value}.`); + }); + }); + + new Setting(containerEl) + .setName('Upcoming Minutes') + .setDesc('The number of minutes to display as upcoming task. Default is 15 minutes.') + .addSlider((slider) => { + let timeoutId: NodeJS.Timeout | null = null; + slider + .setValue(pluginSettings.displaySettings.upcomingMinutes) + .setLimits(0, 60, 1) + .setDynamicTooltip() + .onChange(async (value: number) => { + await writeSettings( + (old) => (old.displaySettings.upcomingMinutes = value) + ); + logger.info(`Upcoming minutes updated: ${value}`); + + // Clear the existing timeout if there is one + if (timeoutId !== null) { + clearTimeout(timeoutId); + } + + // Set a new timeout + timeoutId = setTimeout(() => { + new Notice(`[TaskCard] Upcoming minutes updated: ${value}.`); + // Reset timeoutId to null when the notice is shown + timeoutId = null; + }, 2000); // 2000 milliseconds = 2 seconds delay + }); + }); +} \ No newline at end of file diff --git a/src/settings/syncSettings/googleCalendarSettings.ts b/src/settings/syncSettings/googleCalendarSettings.ts index 14ffb87..6c0af7c 100644 --- a/src/settings/syncSettings/googleCalendarSettings.ts +++ b/src/settings/syncSettings/googleCalendarSettings.ts @@ -4,7 +4,7 @@ import { PluginSettingTab, ButtonComponent, Setting, Notice } from 'obsidian'; / import { GoogleCalendarAuthenticator } from '../../api/googleCalendarAPI/authentication'; import { ProjectModule } from '../../taskModule/project'; import { GoogleCalendarAPI } from '../../api/googleCalendarAPI/calendarAPI'; -import { SettingStore, SyncSetting } from '../../settings'; +import { SettingStore, SyncSetting, TaskCardSettings } from '../../settings'; import { logger } from '../../utils/log'; export interface GoogleSyncSetting extends SyncSetting { @@ -18,7 +18,7 @@ export interface GoogleSyncSetting extends SyncSetting { export async function googleCalendarSyncSettings( containerEl: HTMLElement, - pluginSettings: any, // Adjust with the specific type. + pluginSettings: TaskCardSettings, writeSettings: Function, display: Function, projectModule: ProjectModule, From 794bf803fb403ba88387237694d63ebe7774bc04 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Fri, 27 Oct 2023 13:46:23 -0700 Subject: [PATCH 07/20] =?UTF-8?q?=E2=9C=A8=20more=20display=20modes=20for?= =?UTF-8?q?=20due?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/ui/Due.svelte | 73 ++++++++++++++++++++++++++++++++++-------- src/ui/Duration.svelte | 4 +-- 2 files changed, 62 insertions(+), 15 deletions(-) diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index 7e14d3c..b44bd62 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -152,24 +152,51 @@ return timeGap > 0 && timeGap < minuteGap; } - function isOngoing(due: DueDate, duration: Duration = undefined, currTime: moment.Moment): boolean { - if (!due || !due.date || !due.time || !duration) { return false; } - // convert due to moment + enum TaskDueStatus { + Upcoming = "upcoming", + Ongoing = "ongoing", + PassDue = "passDue", + Passed= "passed" + } + + function getTaskDueStatus(task: ObsidianTask, minuteGap: number = 15): TaskDueStatus | null { + const due = task.due; + let duration = task.duration; + const currTime = moment(); + const finished = task.completed; + if (!due) { return null; } const dueMoment = convertDueToMoment(due); - // convert duration to moment - const endMoment = currTime.clone().add(duration.hours, 'hours').add(duration.minutes, 'minutes'); - // calculate if the time gap is within the specified range - return currTime.isBetween(dueMoment, endMoment); + const timeGap = moment.duration(dueMoment.diff(currTime)).asMinutes(); + // upcoming + if (timeGap > 0 && timeGap < minuteGap) { + return TaskDueStatus.Upcoming; + } + if (!duration) { duration = { hours: 0, minutes: 0 }; } + if (!duration.hours) { duration.hours = 0; } + if (!duration.minutes) { duration.minutes = 0; } + + // ongoing + const endMoment = dueMoment.clone().add(duration.hours, 'hours').add(duration.minutes, 'minutes'); + if (currTime.isBetween(dueMoment, endMoment)) { + return TaskDueStatus.Ongoing; + } + // pass due + if (currTime.isAfter(endMoment) && !finished) { + return TaskDueStatus.PassDue; + } + // passed + if (currTime.isAfter(endMoment) && finished) { + return TaskDueStatus.Passed; + } + return null; } - const now = moment(); - const upcoming = isUpcoming(due, now); - const ongoing = isOngoing(due, duration, now); + const taskDueStatus = getTaskDueStatus(taskSyncManager.obsidianTask, 15); {#if displayDue} -
- +
@@ -190,7 +217,7 @@ class="task-card-due" /> {:else} -
+
{dueDisplay}
@@ -226,6 +253,14 @@ color: var(--text-on-accent); } + .task-card-due-prefix.passDue { + color: var(--text-warning); + } + + .task-card-due-prefix.passed { + color: var(--text-faint); + } + .task-card-due-prefix.ongoing:hover { color: var(--text-on-accent-hover); } @@ -243,6 +278,10 @@ background-color: var(--interactive-accent); } + .task-card-due-container.ongoing:hover { + background-color: var(--interactive-accent-hover); + } + .task-card-due-container.mode-multi-line { margin-top: 2px; } @@ -272,6 +311,14 @@ text-decoration: underline; } + .task-card-due.passDue { + color: var(--text-warning); + } + + .task-card-due.passed { + color: var(--text-faint); + } + .task-card-due.ongoing { color: var(--text-on-accent); } diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index a173aa7..3939e53 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -18,9 +18,9 @@ function customDurationHumanizer(duration: Duration) { if (duration.hours === 0) { - return `${duration.minutes}mins`; + return `${duration.minutes}min${duration.minutes === 1 ? '' : 's'}`; } else if (duration.minutes === 0) { - return `${duration.hours}hrs`; + return `${duration.hours}hr${duration.hours === 1 ? '' : 's'}`; } else { return `${duration.hours}h ${duration.minutes}m`; } From f1ccb0ff50246b4ad0514e4cc8bdcb864d72d549 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Sat, 28 Oct 2023 23:22:21 -0700 Subject: [PATCH 08/20] =?UTF-8?q?=E2=9C=A8=20bug=20fixes=20and=20logic=20i?= =?UTF-8?q?mprovements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/google-calendar-sync-setup.md | 4 +-- src/api/googleCalendarAPI/authentication.ts | 11 +++++-- src/api/googleCalendarAPI/calendarAPI.ts | 20 +++++------ .../syncSettings/googleCalendarSettings.ts | 33 +++++++++++++++---- src/taskModule/taskMonitor.ts | 2 +- src/taskModule/taskParser.ts | 4 +-- src/ui/Duration.svelte | 2 +- 7 files changed, 51 insertions(+), 25 deletions(-) diff --git a/docs/google-calendar-sync-setup.md b/docs/google-calendar-sync-setup.md index 2f20ac4..dd95fe2 100644 --- a/docs/google-calendar-sync-setup.md +++ b/docs/google-calendar-sync-setup.md @@ -3,7 +3,7 @@ # Create a New Client of Google Calendar to Use Google Calendar Sync of Task Card Obsidian ## Step 1: Create a new project in Google Cloud Platform - +TODO: add msg to encourage users - Go to [Google Cloud Platform Console](https://console.cloud.google.com/welcome/); - Create a project: - Navigate to [Create Project](https://console.cloud.google.com/projectcreate); @@ -31,7 +31,7 @@ - **Scope** = `.../auth/userinfo.email`; - **Scope** = `.../auth/userinfo.profile`; - **Scope** = `openid`; - - **API** = `Google Calendar API` (you can search for API name in the filter); + - **API** = `Google Calendar API` (you can search for API name in the filter); TODO: more accurate description - Click on **Update**; - You should see 3 fields in **Your non-sensitive scopes**: - `.../auth/userinfo.email` diff --git a/src/api/googleCalendarAPI/authentication.ts b/src/api/googleCalendarAPI/authentication.ts index 5a7a86a..bad7ab8 100644 --- a/src/api/googleCalendarAPI/authentication.ts +++ b/src/api/googleCalendarAPI/authentication.ts @@ -41,8 +41,9 @@ export class GoogleCalendarAuthenticator { const authURL = this.getAuthUrl(clientID, this.authSession); // Ensure no server is running before starting a new one - if (!this.authSession.server) { - window.open(authURL); + if (this.authSession.server) { + // Close the existing server + this.authSession.server.close(); } return this.startServerAndHandleResponse(this.syncSettings, authURL); @@ -119,6 +120,7 @@ export class GoogleCalendarAuthenticator { + '&code_challenge_method=S256' + '&scope=email%20profile%20https://www.googleapis.com/auth/calendar'; } + async startServerAndHandleResponse(syncSettings: GoogleSyncSetting, authURL: string): Promise { let isResolved = false; // Flag to ensure the promise only resolves once. @@ -180,7 +182,7 @@ export class GoogleCalendarAuthenticator { } }).listen(GoogleCalendarAuthenticator.PORT, () => { - window.open(authURL) + window.open(authURL); setTimeout(() => { if (!isResolved) { isResolved = true; @@ -216,6 +218,9 @@ export class GoogleCalendarAuthenticator { } closeServer() { + if (!this.authSession.server) { + return; + } this.authSession.server.close(() => { logger.info("Server closed.") }); diff --git a/src/api/googleCalendarAPI/calendarAPI.ts b/src/api/googleCalendarAPI/calendarAPI.ts index 6b2bc4a..b95a567 100644 --- a/src/api/googleCalendarAPI/calendarAPI.ts +++ b/src/api/googleCalendarAPI/calendarAPI.ts @@ -66,10 +66,10 @@ export class GoogleCalendarAPI { // 1. intrinsic filter: task without due date won't be created if (!event.currentState.due?.date) return false; // Optional chaining is used here // 2. setting based filter: if there's filter project or tag, check if the task is in the project or tag - if (this.googleSyncSetting.filterProject) { + if (this.googleSyncSetting.doesNeedFilters && this.googleSyncSetting.filterProject) { if (this.googleSyncSetting.filterProject !== event.currentState.project?.id) return false; } - if (this.googleSyncSetting.filterTag) { + if (this.googleSyncSetting.doesNeedFilters && this.googleSyncSetting.filterTag) { if (!event.currentState.labels?.includes(this.googleSyncSetting.filterTag)) return false; } return true; @@ -79,28 +79,28 @@ export class GoogleCalendarAPI { // local task updates can match to task update, create, or delete if (event.type !== TaskChangeType.UPDATE) return ''; logger.debug(`handling local task update: ${JSON.stringify(event)}`); - logger.debug(`has google sync id: ${event.currentState.metadata.syncMappings.googleSyncSetting.id}`); + logger.debug(`has google sync id: ${event.currentState.metadata.syncMappings.googleSyncSetting?.id}`); logger.debug(`filtered: ${this.filterCreationEvent(event)}`); const googleEvent = this.convertTaskToGoogleEvent(event.currentState); // possible task creation events: 1. no google sync id 2. filter passed - if (!event.currentState.metadata.syncMappings.googleSyncSetting.id) { + if (!event.currentState.metadata.syncMappings.googleSyncSetting?.id) { if (this.filterCreationEvent(event) === false) return ''; logger.debug(`try to create event`) const createdEvent = await this.createEvent(googleEvent); - return createdEvent.id; + return createdEvent?.id || ''; } // possible task deletion events: 1. has google sync id; 2. filter failed - if (event.previousState.metadata.syncMappings.googleSyncSetting.id && !this.filterCreationEvent(event)) { + if (event.previousState.metadata.syncMappings.googleSyncSetting?.id && !this.filterCreationEvent(event)) { logger.debug(`try to delete event`) const deletedEvent = await this.deleteEvent(googleEvent); return ''; } // possible task update events: 1. has google sync id; 2. filter passed - if (event.previousState.metadata.syncMappings.googleSyncSetting.id && this.filterCreationEvent(event)) { + if (event.previousState.metadata.syncMappings.googleSyncSetting?.id && this.filterCreationEvent(event)) { logger.debug(`try to update event`) const updatedEvent = await this.updateEvent(googleEvent); - return updatedEvent.id; + return updatedEvent?.id || ''; } return ''; } @@ -329,9 +329,9 @@ export class GoogleCalendarAPI { } // Preprocess the event - // logger.debug(`event: ${JSON.stringify(event)}`); + logger.debug(`event: ${JSON.stringify(event)}`); const processedEvent = this.preprocessEvent(event, targetCalendar); - // logger.debug(`processedEvent: ${JSON.stringify(processedEvent)}`); + logger.debug(`processedEvent: ${JSON.stringify(processedEvent)}`); try { // Assuming callRequest is accessible here, either as a global function or a method on this class. diff --git a/src/settings/syncSettings/googleCalendarSettings.ts b/src/settings/syncSettings/googleCalendarSettings.ts index 6c0af7c..395785a 100644 --- a/src/settings/syncSettings/googleCalendarSettings.ts +++ b/src/settings/syncSettings/googleCalendarSettings.ts @@ -1,7 +1,6 @@ -import { PluginSettingTab, ButtonComponent, Setting, Notice } from 'obsidian'; // Assuming obsidian types based on the code context. -import { GoogleCalendarAuthenticator } from '../../api/googleCalendarAPI/authentication'; +import { ButtonComponent, Setting, Notice } from 'obsidian'; // Assuming obsidian types based on the code context. import { ProjectModule } from '../../taskModule/project'; import { GoogleCalendarAPI } from '../../api/googleCalendarAPI/calendarAPI'; import { SettingStore, SyncSetting, TaskCardSettings } from '../../settings'; @@ -28,7 +27,7 @@ export async function googleCalendarSyncSettings( let loginButton: ButtonComponent; // 1. Google login settings - new Setting(containerEl) + const googleLoginSettings = new Setting(containerEl) .setName("Login via Google") .addText(text => { text @@ -45,7 +44,7 @@ export async function googleCalendarSyncSettings( 'a', { text: 'the documentation', - href: 'https://github.com/terryli710/Obsidian-TaskCard/blob/feature/google-calendar-api/docs/google-calendar-sync-setup.md', + href: 'https://github.com/terryli710/Obsidian-TaskCard/blob/main/docs/google-calendar-sync-setup.md', } ); frag.appendText(' for more details.'); @@ -59,7 +58,29 @@ export async function googleCalendarSyncSettings( }) .setDisabled(googleSettings.isLogin); }) - .addButton(button => { + + if (googleSettings.isLogin) { + googleLoginSettings.addButton(button => { + button + .setButtonText("Test Login") + .onClick(async () => { + if (!googleCalendarAPI) { + new Notice(`[TaskCard] Google Calendar API is not defined.`); + return; + } + const calendars = googleCalendarAPI.listCalendars(); + if (!calendars) { + new Notice(`[TaskCard] Failed to list calendars. Login status test failed.`); + return; + } else { + new Notice(`[TaskCard] Login status test passed.`); + } + + }) + }) + } + + googleLoginSettings.addButton(button => { const isLoggedIn = googleSettings.isLogin; loginButton = button @@ -70,7 +91,7 @@ export async function googleCalendarSyncSettings( googleSettings.isLogin = false; writeSettings(old => old.syncSettings.googleSyncSetting.isLogin = false); } else { - const loginSuccess = await new GoogleCalendarAuthenticator().login(); + const loginSuccess = await googleCalendarAPI.authenticator.login(); if (loginSuccess) { googleSettings.isLogin = true; writeSettings(old => old.syncSettings.googleSyncSetting.isLogin = true); diff --git a/src/taskModule/taskMonitor.ts b/src/taskModule/taskMonitor.ts index a67153f..710fd81 100644 --- a/src/taskModule/taskMonitor.ts +++ b/src/taskModule/taskMonitor.ts @@ -70,7 +70,7 @@ export class TaskMonitor { const taskDetails = this.detectTasksFromLines(lines); if (taskDetails.length === 0) return; for (const taskDetail of taskDetails) { - logger.debug(`taskDetail: ${JSON.stringify(taskDetail)}`); + // logger.debug(`taskDetail: ${JSON.stringify(taskDetail)}`); // notify API about creation of tasks let task = this.parseTaskWithLines(taskDetail.taskMarkdown.split('\n')); const syncMetadata = await this.plugin.externalAPIManager.createTask(task); diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index af1e0f4..35ed2d7 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -72,7 +72,7 @@ export class TaskParser { } } - logger.debug(`Parsing task element: ${taskEl.outerHTML}`); + // logger.debug(`Parsing task element: ${taskEl.outerHTML}`); const task = new ObsidianTask(); const attributes = parseAttributes.bind(this)(); @@ -227,7 +227,7 @@ export class TaskParser { while (allLinesStartWithSpace(descLines)) { descLines = removeLeadingSpace(descLines); } - logger.debug(`Multi-line task: ${descLines.join('\n')}`); + // logger.debug(`Multi-line task: ${descLines.join('\n')}`); task.description = descLines.join('\n'); taskMarkdown = lines[0]; // The first line } diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index 3939e53..167aa51 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -86,7 +86,7 @@ // Main logic for the Enter key if (event.key === 'Enter') { - logger.debug(`duration finished: ${durationInputString}`); + // logger.debug(`duration finished: ${durationInputString}`); // Early exit for empty string or 00:00 duration if (durationInputString.trim() === '' || isValidZeroDuration(durationInputString)) { From 65f28334690c81d7a95560068faada1193526432 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Mon, 30 Oct 2023 21:14:49 -0700 Subject: [PATCH 09/20] =?UTF-8?q?=E2=9C=A8=20change=20name=20from=20due=20?= =?UTF-8?q?to=20schedule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 14 +-- src/api/googleCalendarAPI/calendarAPI.ts | 24 ++-- src/autoSuggestions/Suggester.ts | 38 +++--- src/query/cache.ts | 30 ++--- src/query/querySyncManager.ts | 12 +- src/renderer/TaskCardRenderer.ts | 2 +- src/taskModule/task.ts | 14 +-- src/taskModule/taskParser.ts | 16 +-- src/taskModule/taskSyncManager.ts | 6 +- src/ui/Duration.svelte | 6 +- src/ui/QueryEditor.svelte | 18 +-- src/ui/{Due.svelte => Schedule.svelte} | 148 +++++++++++------------ src/ui/StaticTaskCard.svelte | 34 +++--- src/ui/StaticTaskMatrix.svelte | 13 ++ src/ui/TaskCard.svelte | 22 ++-- tests/suggester.test.ts | 8 +- tests/taskFormatter.test.ts | 6 +- tests/taskParser.test.ts | 28 ++--- tests/taskValidator.test.ts | 4 +- 19 files changed, 228 insertions(+), 215 deletions(-) rename src/ui/{Due.svelte => Schedule.svelte} (65%) create mode 100644 src/ui/StaticTaskMatrix.svelte diff --git a/README.md b/README.md index eec9b8c..e986a73 100644 --- a/README.md +++ b/README.md @@ -33,13 +33,13 @@ Obsidian-TaskCard is an Obsidian plugin designed to revolutionize your task mana ## Features -- **Intuitive and easy-to-use**: the plugin doesn't deviate you from *normal markdown task workflow*. You can create, modify, delete your tasks very similarly when you are using pure markdown in Obsidian. Just by adding a tag (indicator tag in the settings) you can turn your tasks into a task card, which supports two display modes and that allows you to see and edit all attributes of a task, such as the project, due date, and description. +- **Intuitive and easy-to-use**: the plugin doesn't deviate you from *normal markdown task workflow*. You can create, modify, delete your tasks very similarly when you are using pure markdown in Obsidian. Just by adding a tag (indicator tag in the settings) you can turn your tasks into a task card, which supports two display modes and that allows you to see and edit all attributes of a task, such as the project, schedule date, and description. - **Two Display Modes**: Choose between two display modes for your tasks. - **Preview Mode**: Designed for quick browsing, this mode displays tasks at the same height as a normal markdown task, showing only the most essential information. - - **Detailed Mode**: This mode provides a comprehensive task card that allows you to see and edit all attributes of a task, such as the project, due date, and description. + - **Detailed Mode**: This mode provides a comprehensive task card that allows you to see and edit all attributes of a task, such as the project, schedule date, and description. -- **Due Date**: Add a due date to your tasks to indicate when the task is due. +- **Schedule Date**: Add a schedule date to your tasks to indicate when the task is schedule. - **Tags and Projects**: Easily categorize your tasks with tags and associate them with specific projects. @@ -76,7 +76,7 @@ Attributes | Addition | Example | Content | Task in markdown | `- [ ] some content` | Tag | Tag in markdown | `- [ ] some content #tag` | Description | Description in markdown (change line + indent) | `- [ ] some content \n - some description` | -Due Date | Special attribute: `due` | `%%* due: 2021-01-01 *%%` | +Schedule Date | Special attribute: `schedule` | `%%* schedule: 2021-01-01 *%%` | Project | Special attribute: `project` | `%%* project: project name *%%` | #### Create a task @@ -95,15 +95,15 @@ Some attributes are native for a markdown task, we can add them to the task in t #### Add special attributes to a task Some added ingredients for a task card, we can add them in a special way: `%%* key: value *%%`. this is will show nicely in the editing mode of obsidian, while invisible in the preview mode. -- Due Date: Add a due date to the task. e.g. `%%* due: 2021-01-01 *%%` +- Schedule Date: Add a schedule date to the task. e.g. `%%* schedule: 2021-01-01 *%%` - Project: Add a project to the task. e.g. `%%* project: project name *%%` ### Task Modification - Tasks are shown in two view: preview and detailed views. Most attributes are editable in the detailed view. -- Add `description`, `due`, and `project`: click the ⋮ button in the bottom right corner. +- Add `description`, `schedule`, and `project`: click the ⋮ button in the bottom right corner. - Add `tags`: click the + button. - Add `priority`: right click the checkbox. -- Modify `description`, `due`: click on them. +- Modify `description`, `schedule`: click on them. - Modify `tags`: right click on the tag and select `edit`. - Modify `project`: click on the project color dot. - Modify `priority`: right click on the checkbox. diff --git a/src/api/googleCalendarAPI/calendarAPI.ts b/src/api/googleCalendarAPI/calendarAPI.ts index b95a567..fa53007 100644 --- a/src/api/googleCalendarAPI/calendarAPI.ts +++ b/src/api/googleCalendarAPI/calendarAPI.ts @@ -63,8 +63,8 @@ export class GoogleCalendarAPI { filterCreationEvent(event: TaskChangeEvent): boolean { // logic to filter creation events: some events should not be created by google calendar - // 1. intrinsic filter: task without due date won't be created - if (!event.currentState.due?.date) return false; // Optional chaining is used here + // 1. intrinsic filter: task without schedule date won't be created + if (!event.currentState.schedule?.date) return false; // Optional chaining is used here // 2. setting based filter: if there's filter project or tag, check if the task is in the project or tag if (this.googleSyncSetting.doesNeedFilters && this.googleSyncSetting.filterProject) { if (this.googleSyncSetting.filterProject !== event.currentState.project?.id) return false; @@ -127,39 +127,39 @@ export class GoogleCalendarAPI { } constructStartAndEnd(task: Partial): {start: GoogleEventTimePoint, end: GoogleEventTimePoint} { - if (!task.due) { - logger.error(`Task has no due date: ${JSON.stringify(task)}`); + if (!task.schedule) { + logger.error(`Task has no schedule date: ${JSON.stringify(task)}`); return {start: {}, end: {}} } - const due = task.due; + const schedule = task.schedule; const duration = task.duration || { hours: 0, minutes: 0 }; // Default duration if not provided // Format start date and time let startDateTime: string | null = null; - if (due.time) { + if (schedule.time) { // Combine date and time if time is provided - startDateTime = this.dateTimeToGoogleDateTime(`${due.date}T${due.time}`); + startDateTime = this.dateTimeToGoogleDateTime(`${schedule.date}T${schedule.time}`); } const start: GoogleEventTimePoint = { - ...(startDateTime ? { dateTime: startDateTime } : { date: this.dateToGoogleDate(due.date) }), - timeZone: due.timezone || this.defaultCalendar.timeZone || null, + ...(startDateTime ? { dateTime: startDateTime } : { date: this.dateToGoogleDate(schedule.date) }), + timeZone: schedule.timezone || this.defaultCalendar.timeZone || null, }; // Calculate the end time based on the duration const taskDuration = moment.duration({ hours: duration.hours, minutes: duration.minutes }); - const endMoment = moment(startDateTime || due.date).add(taskDuration); // If time is not available, it assumes the start of the day + const endMoment = moment(startDateTime || schedule.date).add(taskDuration); // If time is not available, it assumes the start of the day // Check if duration is longer than one day const isMultiDay = taskDuration.asDays() >= 1; // Determine end date format based on whether time is specified and if duration is less than one day - const endDateFormat = (due.time && !isMultiDay) ? "YYYY-MM-DDTHH:mm:ss" : "YYYY-MM-DD"; + const endDateFormat = (schedule.time && !isMultiDay) ? "YYYY-MM-DDTHH:mm:ss" : "YYYY-MM-DD"; const end: GoogleEventTimePoint = { ...(startDateTime ? { dateTime: endMoment.format(endDateFormat) } : { date: endMoment.format("YYYY-MM-DD") }), - timeZone: due.timezone || this.defaultCalendar.timeZone || null, + timeZone: schedule.timezone || this.defaultCalendar.timeZone || null, }; return { start, end }; diff --git a/src/autoSuggestions/Suggester.ts b/src/autoSuggestions/Suggester.ts index 68df4ce..d3eaf60 100644 --- a/src/autoSuggestions/Suggester.ts +++ b/src/autoSuggestions/Suggester.ts @@ -35,7 +35,7 @@ export class AttributeSuggester { }); this.inputtableAttributes = [ - 'due', + 'schedule', 'duration', 'priority', 'project', @@ -140,13 +140,13 @@ export class AttributeSuggester { getDueSuggestions(lineText: string, cursorPos: number): SuggestInformation[] { let suggestions: SuggestInformation[] = []; - const dueRegexText = `${escapeRegExp(this.startingNotation)}\\s?due:(.*?)${escapeRegExp(this.endingNotation)}`; - const dueRegex = new RegExp(dueRegexText, 'g'); - const dueMatch = matchByPositionAndGroup(lineText, dueRegex, cursorPos, 1); - if (!dueMatch) return suggestions; + const scheduleRegexText = `${escapeRegExp(this.startingNotation)}\\s?schedule:(.*?)${escapeRegExp(this.endingNotation)}`; + const scheduleRegex = new RegExp(scheduleRegexText, 'g'); + const scheduleMatch = matchByPositionAndGroup(lineText, scheduleRegex, cursorPos, 1); + if (!scheduleMatch) return suggestions; - const dueQuery = (dueMatch[1] || '').trim(); - const dueQueryLower = dueQuery.toLowerCase(); + const scheduleQuery = (scheduleMatch[1] || '').trim(); + const scheduleQueryLower = scheduleQuery.toLowerCase(); const level1Suggestions = [ 'today', @@ -191,11 +191,11 @@ export class AttributeSuggester { ] const suggestionLevels = [level1Suggestions, level2Suggestions, level3Suggestions, level4Suggestions]; - const levelInfo = determineLevel(dueQueryLower, suggestionLevels); + const levelInfo = determineLevel(scheduleQueryLower, suggestionLevels); const filteredSuggestions = levelInfo.filteredSuggestions; suggestions = filteredSuggestions.map((filteredSuggestion) => { - // Construct the replaceText with the dueQuery, the current suggestion, and a space if not the last level + // Construct the replaceText with the scheduleQuery, the current suggestion, and a space if not the last level const isLastLevel = suggestionLevels[suggestionLevels.length-1].contains(filteredSuggestion.trim()); if (!filteredSuggestion.startsWith(":")) { filteredSuggestion = ` ${filteredSuggestion}`; @@ -203,21 +203,21 @@ export class AttributeSuggester { if (!isLastLevel) { filteredSuggestion = `${filteredSuggestion} `; } - const replaceText = `${this.startingNotation}due: ${dueQuery.trim()}${filteredSuggestion}${this.endingNotation}`; + const replaceText = `${this.startingNotation}schedule: ${scheduleQuery.trim()}${filteredSuggestion}${this.endingNotation}`; // Set the cursor position to be right after the current suggestion let cursorPosition: number; if (isLastLevel) { - cursorPosition = dueMatch.index + `${this.startingNotation}due: ${dueQuery.trim()}${filteredSuggestion}${this.endingNotation}`.length; + cursorPosition = scheduleMatch.index + `${this.startingNotation}schedule: ${scheduleQuery.trim()}${filteredSuggestion}${this.endingNotation}`.length; } else { - cursorPosition = dueMatch.index + `${this.startingNotation}due: ${dueQuery.trim()}${filteredSuggestion}`.length; + cursorPosition = scheduleMatch.index + `${this.startingNotation}schedule: ${scheduleQuery.trim()}${filteredSuggestion}`.length; } return { displayText: filteredSuggestion, replaceText: replaceText, - replaceFrom: dueMatch.index, - replaceTo: dueMatch.index + dueMatch[0].length, + replaceFrom: scheduleMatch.index, + replaceTo: scheduleMatch.index + scheduleMatch[0].length, cursorPosition: cursorPosition }; }); @@ -231,13 +231,13 @@ export class AttributeSuggester { ): SuggestInformation[] { let suggestions: SuggestInformation[] = []; - // Modify regex to capture the due date query + // Modify regex to capture the schedule date query const durationRegexText = `${escapeRegExp(this.startingNotation)}\\s?duration:(.*?)${escapeRegExp(this.endingNotation)}`; const durationRegex = new RegExp(durationRegexText, 'g'); const durationMatch = matchByPositionAndGroup(lineText, durationRegex, cursorPos, 1); if (!durationMatch) return suggestions; // No match - // Get the due date query from the captured group + // Get the schedule date query from the captured group const durationQuery = (durationMatch[1] || '').trim(); const durationStringSelections = [ @@ -250,7 +250,7 @@ export class AttributeSuggester { '3 hours', ]; - // Use the dueQuery to filter the suggestions + // Use the scheduleQuery to filter the suggestions const filteredDurationStrings = durationStringSelections.filter((durationString) => durationString.toLowerCase().startsWith(durationQuery.toLowerCase()) ); @@ -396,8 +396,8 @@ interface LevelInfo { filteredSuggestions: string[]; } -function determineLevel(dueQuery: string, suggestionLevels: string[][]): LevelInfo { - let currentQuery = dueQuery.trim(); +function determineLevel(scheduleQuery: string, suggestionLevels: string[][]): LevelInfo { + let currentQuery = scheduleQuery.trim(); let currentLevel = 0; let filteredSuggestions: string[] = []; diff --git a/src/query/cache.ts b/src/query/cache.ts index 79c4609..570d3e8 100644 --- a/src/query/cache.ts +++ b/src/query/cache.ts @@ -14,7 +14,7 @@ export interface MultipleAttributeTaskQuery { projectQuery?: string[]; labelQuery?: string[]; completedQuery?: boolean[]; - dueDateTimeQuery?: [string, string]; + scheduleDateTimeQuery?: [string, string]; filePathQuery?: string; } @@ -29,7 +29,7 @@ export interface TaskRow { labels: string; completed: boolean; parentID: string; - due: string; + schedule: string; metadata: string; filePath: string; startLine: number; @@ -181,9 +181,9 @@ export class TaskDatabase extends IndexedMapDatabase { this.createIndex('project', item => item.project.name); this.createIndex('labels', item => item.labels.join(',')); this.createIndex('completed', item => item.completed); - this.createIndex('due.date', item => item.due ? item.due.date : null); - this.createIndex('due.time', item => item.due ? item.due.time : null); - this.createIndex('due.string', item => item.due ? item.due.string : null); + this.createIndex('schedule.date', item => item.schedule ? item.schedule.date : null); + this.createIndex('schedule.time', item => item.schedule ? item.schedule.time : null); + this.createIndex('schedule.string', item => item.schedule ? item.schedule.string : null); this.createIndex('filePath', item => item.docPosition.filePath); } @@ -192,7 +192,7 @@ export class TaskDatabase extends IndexedMapDatabase { projectQuery = [], labelQuery = [], completedQuery = [], - dueDateTimeQuery = ['', ''], + scheduleDateTimeQuery = ['', ''], filePathQuery = '', }: MultipleAttributeTaskQuery): PositionedTaskProperties[] { const expression = { @@ -204,30 +204,30 @@ export class TaskDatabase extends IndexedMapDatabase { task => completedQuery && completedQuery.length > 0 ? completedQuery.includes(task.completed) : true, task => { // Case 3: If both start and end are empty strings, return true for all tasks - if (!dueDateTimeQuery || (dueDateTimeQuery.length === 2 && dueDateTimeQuery[0] === '' && dueDateTimeQuery[1] === '')) { + if (!scheduleDateTimeQuery || (scheduleDateTimeQuery.length === 2 && scheduleDateTimeQuery[0] === '' && scheduleDateTimeQuery[1] === '')) { return true; } - const [start, end] = dueDateTimeQuery; + const [start, end] = scheduleDateTimeQuery; - // If the task has a due date - if (task.due) { - const taskDateTime = Sugar.Date.create(task.due.string); + // If the task has a schedule date + if (task.schedule) { + const taskDateTime = Sugar.Date.create(task.schedule.string); - // Case 1: If no start date is provided, filter tasks due before the end date + // Case 1: If no start date is provided, filter tasks schedule before the end date if (start === '') { return Sugar.Date.isBefore(taskDateTime, end) || Sugar.Date.is(taskDateTime, end); } - // Case 2: If no end date is provided, filter tasks due after the start date + // Case 2: If no end date is provided, filter tasks schedule after the start date if (end === '') { return Sugar.Date.isAfter(taskDateTime, start) || Sugar.Date.is(taskDateTime, start); } - // Default case: Filter tasks that are due between the start and end dates (inclusive) + // Default case: Filter tasks that are schedule between the start and end dates (inclusive) return Sugar.Date.isBetween(taskDateTime, start, end) || Sugar.Date.is(taskDateTime, start) || Sugar.Date.is(taskDateTime, end); } - // If the task has no due date, it doesn't meet any of the filtering conditions + // If the task has no schedule date, it doesn't meet any of the filtering conditions return false; }, task => filePathQuery && filePathQuery !== '' ? task.docPosition.filePath.startsWith(filePathQuery) : true diff --git a/src/query/querySyncManager.ts b/src/query/querySyncManager.ts index 56989b6..9f11e59 100644 --- a/src/query/querySyncManager.ts +++ b/src/query/querySyncManager.ts @@ -56,7 +56,7 @@ export class QuerySyncManager { projectQuery: [], labelQuery: [], completedQuery: [], - dueDateTimeQuery: ['', ''], + scheduleDateTimeQuery: ['', ''], filePathQuery: '', }; // this.initOptions(); @@ -69,7 +69,7 @@ export class QuerySyncManager { projectQuery: [], labelQuery: [], completedQuery: [], - dueDateTimeQuery: ['', ''], + scheduleDateTimeQuery: ['', ''], filePathQuery: '', }; @@ -87,8 +87,8 @@ export class QuerySyncManager { query.labelQuery = JSON.parse(value); } else if (key === 'completed') { query.completedQuery = JSON.parse(value); - } else if (key === 'due') { - query.dueDateTimeQuery = JSON.parse(value); + } else if (key === 'schedule') { + query.scheduleDateTimeQuery = JSON.parse(value); } else if (key === 'file') { query.filePathQuery = value.replace(/['"]+/g, ''); } else if (key === 'editMode') { @@ -153,8 +153,8 @@ export class QuerySyncManager { source += `completed: ${JSON.stringify(query.completedQuery)}\n`; } - if (query.dueDateTimeQuery !== undefined && query.dueDateTimeQuery !== null) { - source += `due: ${JSON.stringify(query.dueDateTimeQuery)}\n`; + if (query.scheduleDateTimeQuery !== undefined && query.scheduleDateTimeQuery !== null) { + source += `schedule: ${JSON.stringify(query.scheduleDateTimeQuery)}\n`; } if (query.filePathQuery !== undefined && query.filePathQuery !== null) { diff --git a/src/renderer/TaskCardRenderer.ts b/src/renderer/TaskCardRenderer.ts index 3163701..46d2f9d 100644 --- a/src/renderer/TaskCardRenderer.ts +++ b/src/renderer/TaskCardRenderer.ts @@ -97,7 +97,7 @@ taskCardStatus: { descriptionStatus: 'done', projectStatus: 'done', - dueStatus: 'done', + scheduleStatus: 'done', durationStatus: 'done' }, markdownTask: null, diff --git a/src/taskModule/task.ts b/src/taskModule/task.ts index c85dace..8bbe320 100644 --- a/src/taskModule/task.ts +++ b/src/taskModule/task.ts @@ -41,7 +41,7 @@ export interface TaskProperties { parent?: TaskProperties | ObsidianTask | null; children: TaskProperties[] | ObsidianTask[]; - due?: DueDate | null; + schedule?: DueDate | null; duration?: Duration | null; metadata?: { taskDisplayParams?: TaskDisplayParams | null; @@ -64,7 +64,7 @@ export class ObsidianTask implements TaskProperties { public parent?: TaskProperties | ObsidianTask | null; public children: TaskProperties[] | ObsidianTask[]; - public due?: DueDate | null; + public schedule?: DueDate | null; public duration?: Duration | null; public metadata?: { @@ -85,7 +85,7 @@ export class ObsidianTask implements TaskProperties { this.completed = props?.completed || false; this.parent = props?.parent || null; this.children = props?.children || []; - this.due = props?.due || null; + this.schedule = props?.schedule || null; this.duration = props?.duration || null; this.metadata = props?.metadata || {}; } @@ -103,7 +103,7 @@ export class ObsidianTask implements TaskProperties { completed: this.completed, parent: this.parent, children: this.children, - due: this.due, + schedule: this.schedule, duration: this.duration, metadata: this.metadata, }); @@ -134,9 +134,9 @@ export class ObsidianTask implements TaskProperties { } hasDue(): boolean { - if (!this.due) return false; - // return if the due string is not empty - return !!this.due.string.trim(); + if (!this.schedule) return false; + // return if the schedule string is not empty + return !!this.schedule.string.trim(); } hasDuration(): boolean { diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index 35ed2d7..bed10a8 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -87,7 +87,7 @@ export class TaskParser { task.labels = attributes.labels || []; task.parent = attributes.parent || null; task.children = attributes.children || []; - task.due = attributes.due || null; + task.schedule = attributes.schedule || null; task.duration = attributes.duration || null; task.metadata = attributes.metadata || {}; @@ -281,8 +281,8 @@ export class TaskParser { } switch (attributeName) { - case 'due': - task.due = tryParseAttribute('due', this.parseDue.bind(this), attributeValue, 'other'); + case 'schedule': + task.schedule = tryParseAttribute('schedule', this.parseDue.bind(this), attributeValue, 'other'); break; case 'duration': task.duration = tryParseAttribute('duration', this.parseDuration.bind(this), attributeValue, 'other'); @@ -373,7 +373,7 @@ export class TaskParser { task.priority = parseJSONAttribute(metadata['priority'], 'priority', '4' as unknown as Priority); task.order = parseJSONAttribute(metadata['order'], 'order', 0); task.project = parseJSONAttribute(metadata['project'], 'project', null); - task.due = parseJSONAttribute(metadata['due'], 'due', null); + task.schedule = parseJSONAttribute(metadata['schedule'], 'schedule', null); task.duration = parseJSONAttribute(metadata['duration'], 'duration', null); task.metadata = parseJSONAttribute(metadata['metadata'], 'metadata', {}); @@ -440,8 +440,8 @@ export class TaskParser { } - parseDue(dueString: string): DueDate | null { - const parsedDateTime = Sugar.Date.create(dueString); + parseDue(scheduleString: string): DueDate | null { + const parsedDateTime = Sugar.Date.create(scheduleString); // Check if the parsedDateTime is a valid date if (!parsedDateTime || !Sugar.Date.isValid(parsedDateTime)) { @@ -457,14 +457,14 @@ export class TaskParser { return { isRecurring: false, date: parsedDate, - string: dueString + string: scheduleString } as DueDate; } else { return { isRecurring: false, date: parsedDate, time: parsedTime, - string: dueString + string: scheduleString } as DueDate; } } diff --git a/src/taskModule/taskSyncManager.ts b/src/taskModule/taskSyncManager.ts index dd00045..f387b3d 100644 --- a/src/taskModule/taskSyncManager.ts +++ b/src/taskModule/taskSyncManager.ts @@ -7,7 +7,7 @@ import { logger } from '../utils/log'; type TaskCardStatus = { descriptionStatus: 'editing' | 'done'; projectStatus: 'selecting' | 'done'; - dueStatus: 'editing' | 'done'; + scheduleStatus: 'editing' | 'done'; durationStatus: 'editing' | 'done'; }; @@ -46,7 +46,7 @@ export class ObsidianTaskSyncManager implements ObsidianTaskSyncProps { this.taskCardStatus = props?.taskCardStatus || { descriptionStatus: 'done', projectStatus: 'done', - dueStatus: 'done', + scheduleStatus: 'done', durationStatus: 'done', }; this.taskItemEl = props?.taskItemEl || null; @@ -125,7 +125,7 @@ export class ObsidianTaskSyncManager implements ObsidianTaskSyncProps { const allowedStatuses = { descriptionStatus: ['editing', 'done'], projectStatus: ['selecting', 'done'], - dueStatus: ['editing', 'done'], + scheduleStatus: ['editing', 'done'], durationStatus: ['editing', 'done'] }; return allowedStatuses[key].includes(status); diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index 167aa51..4511e32 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -131,7 +131,7 @@ function parseDurationInput(input: string): { hours: number, minutes: number } | node.select(); } - let displayDue: boolean = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'; + let displayDue: boolean = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; $: displayDuration = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; @@ -140,7 +140,7 @@ function parseDurationInput(input: string): { hours: number, minutes: number } | {#if displayDuration} {#if displayDue} - + {/if}
handleSelection(evt, 'label')} /> - +
  • -
    Due Date
    -
    To filter by due date
    +
    Schedule Date
    +
    To filter by schedule date
    diff --git a/src/ui/Due.svelte b/src/ui/Schedule.svelte similarity index 65% rename from src/ui/Due.svelte rename to src/ui/Schedule.svelte index b44bd62..2fc5c24 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Schedule.svelte @@ -15,19 +15,19 @@ export let params: TaskDisplayParams; export let displayDue: boolean; - let due: DueDate | null; - let dueString: string; + let schedule: DueDate | null; + let scheduleString: string; let duration: Duration | null; - let dueDisplay = ""; + let scheduleDisplay = ""; let inputElement: HTMLInputElement; - due = taskSyncManager.obsidianTask.hasDue() ? taskSyncManager.obsidianTask.due : null; - dueString = due ? due.string : ''; + schedule = taskSyncManager.obsidianTask.hasDue() ? taskSyncManager.obsidianTask.schedule : null; + scheduleString = schedule ? schedule.string : ''; duration = taskSyncManager.obsidianTask.hasDuration() ? taskSyncManager.obsidianTask.duration : null; updateDueDisplay(); async function toggleEditMode(event: KeyboardEvent | MouseEvent) { - if (taskSyncManager.taskCardStatus.dueStatus === 'done') { + if (taskSyncManager.taskCardStatus.scheduleStatus === 'done') { enableDueEditMode(event); } else { finishDueEditing(event); @@ -43,9 +43,9 @@ event.preventDefault(); } } - // taskSyncManager.setTaskCardStatus('dueStatus', 'editing'); - taskSyncManager.taskCardStatus.dueStatus = 'editing'; - dueString = due ? due.string : ''; + // taskSyncManager.setTaskCardStatus('scheduleStatus', 'editing'); + taskSyncManager.taskCardStatus.scheduleStatus = 'editing'; + scheduleString = schedule ? schedule.string : ''; await tick(); focusAndSelect(inputElement); adjustWidthForInput(inputElement); @@ -57,43 +57,43 @@ } if (event.key === 'Enter' || event.key === 'Escape') { - taskSyncManager.taskCardStatus.dueStatus = 'done'; + taskSyncManager.taskCardStatus.scheduleStatus = 'done'; if (event.key === 'Escape') { updateDueDisplay(); return; } - if (dueString.trim() === '') { - due = null; + if (scheduleString.trim() === '') { + schedule = null; } else { try { - let newDue = plugin.taskParser.parseDue(dueString); + let newDue = plugin.taskParser.parseDue(scheduleString); if (newDue) { - due = newDue; + schedule = newDue; } else { - new Notice(`[TaskCard] Invalid due date: ${dueString}`); - dueString = due ? due.string : ''; + new Notice(`[TaskCard] Invalid schedule date: ${scheduleString}`); + scheduleString = schedule ? schedule.string : ''; } } catch (e) { logger.error(e); - dueString = due ? due.string : ''; + scheduleString = schedule ? schedule.string : ''; } } - taskSyncManager.updateObsidianTaskAttribute('due', due); + taskSyncManager.updateObsidianTaskAttribute('schedule', schedule); updateDueDisplay(); } } function updateDueDisplay(): string { - if (!due) { - dueDisplay = ''; - return dueDisplay; + if (!schedule) { + scheduleDisplay = ''; + return scheduleDisplay; } - let datePart = displayDate(due.date); - let timePart = displayTime(due.time); - dueDisplay = timePart ? `${datePart}, ${timePart}` : datePart; - return dueDisplay; + let datePart = displayDate(schedule.date); + let timePart = displayTime(schedule.time); + scheduleDisplay = timePart ? `${datePart}, ${timePart}` : datePart; + return scheduleDisplay; } // Action function to focus and select the input content @@ -125,29 +125,29 @@ } - function convertDueToMoment(dueDate: DueDate): moment.Moment | null { - if (!dueDate.date) { + function convertDueToMoment(scheduleDate: DueDate): moment.Moment | null { + if (!scheduleDate.date) { return null; // Ensure the date is present. } - let datetimeStr = dueDate.date; - if (dueDate.time) { - datetimeStr += ' ' + dueDate.time; + let datetimeStr = scheduleDate.date; + if (scheduleDate.time) { + datetimeStr += ' ' + scheduleDate.time; } return moment(datetimeStr); } - $: displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.taskCardStatus.dueStatus === 'editing'; + $: displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.taskCardStatus.scheduleStatus === 'editing'; // upcoming and ongoing - function isUpcoming(due: DueDate, currTime: moment.Moment, minuteGap: number = 15): boolean { - if (!due || !due.date || !due.time) { return false; } - // convert due to moment - const dueMoment = convertDueToMoment(due); + function isUpcoming(schedule: DueDate, currTime: moment.Moment, minuteGap: number = 15): boolean { + if (!schedule || !schedule.date || !schedule.time) { return false; } + // convert schedule to moment + const scheduleMoment = convertDueToMoment(schedule); // calculate the time gap - const timeGap = moment.duration(dueMoment.diff(currTime)).asMinutes(); + const timeGap = moment.duration(scheduleMoment.diff(currTime)).asMinutes(); // check if the time gap is within the specified range return timeGap > 0 && timeGap < minuteGap; } @@ -160,13 +160,13 @@ } function getTaskDueStatus(task: ObsidianTask, minuteGap: number = 15): TaskDueStatus | null { - const due = task.due; + const schedule = task.schedule; let duration = task.duration; const currTime = moment(); const finished = task.completed; - if (!due) { return null; } - const dueMoment = convertDueToMoment(due); - const timeGap = moment.duration(dueMoment.diff(currTime)).asMinutes(); + if (!schedule) { return null; } + const scheduleMoment = convertDueToMoment(schedule); + const timeGap = moment.duration(scheduleMoment.diff(currTime)).asMinutes(); // upcoming if (timeGap > 0 && timeGap < minuteGap) { return TaskDueStatus.Upcoming; @@ -176,11 +176,11 @@ if (!duration.minutes) { duration.minutes = 0; } // ongoing - const endMoment = dueMoment.clone().add(duration.hours, 'hours').add(duration.minutes, 'minutes'); - if (currTime.isBetween(dueMoment, endMoment)) { + const endMoment = scheduleMoment.clone().add(duration.hours, 'hours').add(duration.minutes, 'minutes'); + if (currTime.isBetween(scheduleMoment, endMoment)) { return TaskDueStatus.Ongoing; } - // pass due + // pass schedule if (currTime.isAfter(endMoment) && !finished) { return TaskDueStatus.PassDue; } @@ -196,30 +196,30 @@ {#if displayDue} -
    -
    - - +
    + +
    - {#if taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} + {#if taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'} adjustWidthForInput(inputElement)} - bind:value={dueString} + bind:value={scheduleString} bind:this={inputElement} - class="task-card-due" + class="task-card-schedule" /> {:else} -
    -
    - {dueDisplay} +
    +
    + {scheduleDisplay}
    {/if} @@ -229,7 +229,7 @@ \ No newline at end of file diff --git a/src/ui/TaskCard.svelte b/src/ui/TaskCard.svelte index ed43dc7..3ca14f9 100644 --- a/src/ui/TaskCard.svelte +++ b/src/ui/TaskCard.svelte @@ -1,5 +1,5 @@ @@ -230,7 +230,7 @@ {#if descriptionProgress[1] * descriptionProgress[0] > 0 && !task.completed } {/if} - +
    @@ -261,7 +261,7 @@
    - + {#if displayDue || displayDuration}
    diff --git a/tests/suggester.test.ts b/tests/suggester.test.ts index c90145b..67f6749 100644 --- a/tests/suggester.test.ts +++ b/tests/suggester.test.ts @@ -55,11 +55,11 @@ describe('AttributeSuggester', () => { expect(suggestions).toHaveLength(4); // Assuming 4 priority suggestions are returned }); - it('gets due suggestions', () => { - const lineText = '{{ due: t }}'; + it('gets schedule suggestions', () => { + const lineText = '{{ schedule: t }}'; const cursorPos = 9; const suggestions = suggester.getDueSuggestions(lineText, cursorPos); - expect(suggestions).toHaveLength(4); // Assuming 4 due suggestions are returned + expect(suggestions).toHaveLength(4); // Assuming 4 schedule suggestions are returned }); it('wont get attribute suggestions if the endingNotation is not found', () => { @@ -70,7 +70,7 @@ describe('AttributeSuggester', () => { }); it('wont get priority suggestions if cursor is not at the correct position', () => { - const lineText = '{{ due: '; + const lineText = '{{ schedule: '; const cursorPos = 6; const suggestions = suggester.getDueSuggestions(lineText, cursorPos); expect(suggestions).toHaveLength(0); diff --git a/tests/taskFormatter.test.ts b/tests/taskFormatter.test.ts index a3c684d..0beace8 100644 --- a/tests/taskFormatter.test.ts +++ b/tests/taskFormatter.test.ts @@ -89,11 +89,11 @@ describe('taskToMarkdown', () => { expect(result).toContain('#label1 #label2'); }); - it('should format a task with due date', () => { + it('should format a task with schedule date', () => { const task = new ObsidianTask({ content: 'An example task', completed: false, - due: { + schedule: { isRecurring: false, date: '2024-08-15', string: '2023-08-15', @@ -101,7 +101,7 @@ describe('taskToMarkdown', () => { } }); const result = taskFormatter.taskToMarkdown(task); - expect(result).toContain('"due":{"isRecurring":false,"date":"2024-08-15","string":"2023-08-15","timezone":null}'); + expect(result).toContain('"schedule":{"isRecurring":false,"date":"2024-08-15","string":"2023-08-15","timezone":null}'); }); it('should format a task with metadata', () => { diff --git a/tests/taskParser.test.ts b/tests/taskParser.test.ts index 9f99db7..79f54d2 100644 --- a/tests/taskParser.test.ts +++ b/tests/taskParser.test.ts @@ -46,7 +46,7 @@ import { Project, ProjectModule } from '../src/taskModule/project'; // labels: ['label1', 'label2'], // parent: null, // children: [], -// due: { +// schedule: { // isRecurring: false, // string: '2023-08-15', // date: '2024-08-15', @@ -201,7 +201,7 @@ describe('taskParser', () => { // completed: false, // parent: null, // children: [], - // due: { + // schedule: { // isRecurring: false, // string: '2023-08-15', // date: '2024-08-15', @@ -242,7 +242,7 @@ describe('taskParser', () => { // completed: false, // parent: null, // children: [], - // due: { + // schedule: { // isRecurring: false, // string: '2023-08-15', // date: '2024-08-15', @@ -315,7 +315,7 @@ describe('taskParser', () => { // describe('parseExtractedFormattedTaskMarkdown', () => { // it('should correctly parse a complete task', () => { - // const taskMarkdown = '- [ ] Exercise #PersonalLife #Health #TaskCard{"id":"69c7847e-b182-4353-8676-d29450dedbdb","priority":1,"description":"- Cardio for 30 mins\\\\n- Weight lifting for 20 mins","order":0,"project":{"id":"bdedc03b-88e8-4a1e-b566-fe12d3d925e7","name":"HealthPlan","color":"#f45fe3"},"sectionID":"","parent":null,"children":[],"due":null,"metadata":{}} .'; + // const taskMarkdown = '- [ ] Exercise #PersonalLife #Health #TaskCard{"id":"69c7847e-b182-4353-8676-d29450dedbdb","priority":1,"description":"- Cardio for 30 mins\\\\n- Weight lifting for 20 mins","order":0,"project":{"id":"bdedc03b-88e8-4a1e-b566-fe12d3d925e7","name":"HealthPlan","color":"#f45fe3"},"sectionID":"","parent":null,"children":[],"schedule":null,"metadata":{}} .'; // const parsedTask = taskParser.parseExtractedFormattedTaskMarkdown(taskMarkdown); // expect(parsedTask.completed).toBe(false); // expect(parsedTask.content).toBe('Exercise'); @@ -328,12 +328,12 @@ describe('taskParser', () => { // expect(parsedTask.sectionID).toBe(''); // expect(parsedTask.parent).toBe(null); // expect(parsedTask.children).toEqual([]); - // expect(parsedTask.due).toBe(null); + // expect(parsedTask.schedule).toBe(null); // expect(parsedTask.metadata).toEqual({}); // }); // it('should handle tasks without metadata', () => { - // const taskMarkdown = '- [ ] Exercise #PersonalLife #Health #TaskCard{"id":"","priority":4,"description":"","order":0,"project":{"id":"","name":"","color":""},"sectionID":"","parent":null,"children":[],"due":null,"metadata":{}} .'; + // const taskMarkdown = '- [ ] Exercise #PersonalLife #Health #TaskCard{"id":"","priority":4,"description":"","order":0,"project":{"id":"","name":"","color":""},"sectionID":"","parent":null,"children":[],"schedule":null,"metadata":{}} .'; // const parsedTask = taskParser.parseExtractedFormattedTaskMarkdown(taskMarkdown); // expect(parsedTask.completed).toBe(false); // expect(parsedTask.content).toBe('Exercise'); @@ -344,7 +344,7 @@ describe('taskParser', () => { // }); // it('should handle completed tasks', () => { - // const taskMarkdown = '- [x] Exercise #PersonalLife #Health #TaskCard{"id":"","priority":4,"description":"","order":0,"project":{"id":"","name":"","color":""},"sectionID":"","parent":null,"children":[],"due":null,"metadata":{}} .'; + // const taskMarkdown = '- [x] Exercise #PersonalLife #Health #TaskCard{"id":"","priority":4,"description":"","order":0,"project":{"id":"","name":"","color":""},"sectionID":"","parent":null,"children":[],"schedule":null,"metadata":{}} .'; // const parsedTask = taskParser.parseExtractedFormattedTaskMarkdown(taskMarkdown); // expect(parsedTask.completed).toBe(true); // }); @@ -404,16 +404,16 @@ describe('taskParser', () => { expect(parsedTask).toMatchObject(expectedTask); }); - // 3. Parsing a task with a `due` attribute. - it('should parse a task with a due attribute correctly', () => { - const taskMarkdown = '- [ ] Task with due date %%*due:2023-08-05*%%'; + // 3. Parsing a task with a `schedule` attribute. + it('should parse a task with a schedule attribute correctly', () => { + const taskMarkdown = '- [ ] Task with schedule date %%*schedule:2023-08-05*%%'; const parsedTask = taskParser.parseTaskMarkdown(taskMarkdown); const expectedTask = { - content: 'Task with due date', + content: 'Task with schedule date', completed: false, - due: { + schedule: { isRecurring: false, date: '2023-08-05', string: '2023-08-05' @@ -545,14 +545,14 @@ describe('taskParser', () => { // 11. Parsing a task with multiple attributes. it('should parse a task with multiple attributes correctly', () => { const taskMarkdown = - '- [ ] Multi-attribute task %%*priority:2*%% %%*due:2023-09-01*%%'; + '- [ ] Multi-attribute task %%*priority:2*%% %%*schedule:2023-09-01*%%'; const parsedTask = taskParser.parseTaskMarkdown(taskMarkdown); const expectedTask = { content: 'Multi-attribute task', priority: 2, - due: { + schedule: { isRecurring: false, date: '2023-09-01', string: '2023-09-01' diff --git a/tests/taskValidator.test.ts b/tests/taskValidator.test.ts index faa7d4c..b79991d 100644 --- a/tests/taskValidator.test.ts +++ b/tests/taskValidator.test.ts @@ -84,7 +84,7 @@ describe('TaskValidator', () => { // labels: includeAllAttributes || Math.random() > 0.5 ? ['label1', 'label2'] : null, // parent: null, // children: [], - // due: includeAllAttributes || Math.random() > 0.5 ? { + // schedule: includeAllAttributes || Math.random() > 0.5 ? { // isRecurring: false, // string: '2023-08-15', // date: '2024-08-15', @@ -176,7 +176,7 @@ describe('TaskValidator', () => { %%*description:"- A multi line description.\n- the second line."*%% %%*order:1*%% %%*project:{"id":"project-123", "name":"Project Name"}*%% %%*section-id:"section-456"*%% %%*parent:null*%% %%*children:[]*%% - %%*due:"Aug 15, 2024"*%% + %%*schedule:"Aug 15, 2024"*%% %%*metadata:{"filePath":"/path/to/file"}*%%`; const onelineUnformattedTask = unformattedTask.replace(/\n/g, ''); From 2a8d3fd497f1588e05b963563a09f6cc5b4d3648 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Mon, 30 Oct 2023 21:18:56 -0700 Subject: [PATCH 10/20] =?UTF-8?q?=E2=9C=A8=20Due=20to=20Schedule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/autoSuggestions/Suggester.ts | 4 +- src/query/cache.ts | 2 +- src/taskModule/task.ts | 8 ++-- src/taskModule/taskParser.ts | 10 ++--- src/ui/Duration.svelte | 6 +-- src/ui/Schedule.svelte | 68 ++++++++++++++++---------------- src/ui/StaticTaskCard.svelte | 8 ++-- src/ui/TaskCard.svelte | 14 +++---- tests/suggester.test.ts | 4 +- 9 files changed, 62 insertions(+), 62 deletions(-) diff --git a/src/autoSuggestions/Suggester.ts b/src/autoSuggestions/Suggester.ts index d3eaf60..21fb23f 100644 --- a/src/autoSuggestions/Suggester.ts +++ b/src/autoSuggestions/Suggester.ts @@ -53,7 +53,7 @@ export class AttributeSuggester { this.getPrioritySuggestions(lineText, cursorPos) ); suggestions = suggestions.concat( - this.getDueSuggestions(lineText, cursorPos) + this.getScheduleSuggestions(lineText, cursorPos) ); suggestions = suggestions.concat( this.getDurationSuggestions(lineText, cursorPos) @@ -137,7 +137,7 @@ export class AttributeSuggester { } - getDueSuggestions(lineText: string, cursorPos: number): SuggestInformation[] { + getScheduleSuggestions(lineText: string, cursorPos: number): SuggestInformation[] { let suggestions: SuggestInformation[] = []; const scheduleRegexText = `${escapeRegExp(this.startingNotation)}\\s?schedule:(.*?)${escapeRegExp(this.endingNotation)}`; diff --git a/src/query/cache.ts b/src/query/cache.ts index 570d3e8..490b248 100644 --- a/src/query/cache.ts +++ b/src/query/cache.ts @@ -1,4 +1,4 @@ -import { PositionedTaskProperties, DocPosition, Priority, DueDate, ObsidianTask, TextPosition, PositionedObsidianTask } from '../taskModule/task'; +import { PositionedTaskProperties, DocPosition, Priority, ScheduleDate, ObsidianTask, TextPosition, PositionedObsidianTask } from '../taskModule/task'; import TaskCardPlugin from '..'; import { getAPI } from 'obsidian-dataview'; import { QueryResult } from 'obsidian-dataview/lib/api/plugin-api'; diff --git a/src/taskModule/task.ts b/src/taskModule/task.ts index 8bbe320..0a1b8f6 100644 --- a/src/taskModule/task.ts +++ b/src/taskModule/task.ts @@ -10,7 +10,7 @@ export const DateOnly = String.withConstraint((s) => ); export const TimeOnly = String.withConstraint((s) => /^\d{2}:\d{2}$/.test(s)); -export type DueDate = { +export type ScheduleDate = { isRecurring: boolean; date: Static; time?: Static | null; @@ -41,7 +41,7 @@ export interface TaskProperties { parent?: TaskProperties | ObsidianTask | null; children: TaskProperties[] | ObsidianTask[]; - schedule?: DueDate | null; + schedule?: ScheduleDate | null; duration?: Duration | null; metadata?: { taskDisplayParams?: TaskDisplayParams | null; @@ -64,7 +64,7 @@ export class ObsidianTask implements TaskProperties { public parent?: TaskProperties | ObsidianTask | null; public children: TaskProperties[] | ObsidianTask[]; - public schedule?: DueDate | null; + public schedule?: ScheduleDate | null; public duration?: Duration | null; public metadata?: { @@ -133,7 +133,7 @@ export class ObsidianTask implements TaskProperties { return this.children.length > 0; } - hasDue(): boolean { + hasSchedule(): boolean { if (!this.schedule) return false; // return if the schedule string is not empty return !!this.schedule.string.trim(); diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index bed10a8..ebabffb 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -2,7 +2,7 @@ import { logger } from '../utils/log'; import { escapeRegExp, extractTags } from '../utils/regexUtils'; import { kebabToCamel } from '../utils/stringCaseConverter'; import { toArray, toBoolean } from '../utils/typeConversion'; -import { DueDate, Duration, ObsidianTask, Order, Priority, TaskProperties, TextPosition } from './task'; +import { ScheduleDate, Duration, ObsidianTask, Order, Priority, TaskProperties, TextPosition } from './task'; import { Project, ProjectModule } from './project'; import Sugar from 'sugar'; import { SettingStore } from '../settings'; @@ -282,7 +282,7 @@ export class TaskParser { switch (attributeName) { case 'schedule': - task.schedule = tryParseAttribute('schedule', this.parseDue.bind(this), attributeValue, 'other'); + task.schedule = tryParseAttribute('schedule', this.parseSchedule.bind(this), attributeValue, 'other'); break; case 'duration': task.duration = tryParseAttribute('duration', this.parseDuration.bind(this), attributeValue, 'other'); @@ -440,7 +440,7 @@ export class TaskParser { } - parseDue(scheduleString: string): DueDate | null { + parseSchedule(scheduleString: string): ScheduleDate | null { const parsedDateTime = Sugar.Date.create(scheduleString); // Check if the parsedDateTime is a valid date @@ -458,14 +458,14 @@ export class TaskParser { isRecurring: false, date: parsedDate, string: scheduleString - } as DueDate; + } as ScheduleDate; } else { return { isRecurring: false, date: parsedDate, time: parsedTime, string: scheduleString - } as DueDate; + } as ScheduleDate; } } diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index 4511e32..93bda76 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -1,7 +1,7 @@ -{#if displayDue} -
    - +
    @@ -217,7 +217,7 @@ class="task-card-schedule" /> {:else} -
    +
    {scheduleDisplay}
    @@ -253,7 +253,7 @@ color: var(--text-on-accent); } - .task-card-schedule-prefix.passDue { + .task-card-schedule-prefix.passSchedule { color: var(--text-warning); } @@ -311,7 +311,7 @@ text-decoration: underline; } - .task-card-schedule.passDue { + .task-card-schedule.passSchedule { color: var(--text-warning); } diff --git a/src/ui/StaticTaskCard.svelte b/src/ui/StaticTaskCard.svelte index 0c0f6a6..f560735 100644 --- a/src/ui/StaticTaskCard.svelte +++ b/src/ui/StaticTaskCard.svelte @@ -57,7 +57,7 @@ ); } - function updateDueDisplay(): string { + function updateScheduleDisplay(): string { if (!task.schedule) { scheduleDisplay = ''; return scheduleDisplay; @@ -68,7 +68,7 @@ return scheduleDisplay; } - updateDueDisplay(); + updateScheduleDisplay(); function switchMode( event: MouseEvent | KeyboardEvent | CustomEvent, @@ -132,7 +132,7 @@ {/if} - {#if task.hasDue()} + {#if task.hasSchedule()}
    {scheduleDisplay} @@ -223,7 +223,7 @@
    - {#if task.hasDue()} + {#if task.hasSchedule()}
    {scheduleDisplay} diff --git a/src/ui/TaskCard.svelte b/src/ui/TaskCard.svelte index 3ca14f9..a0138b6 100644 --- a/src/ui/TaskCard.svelte +++ b/src/ui/TaskCard.svelte @@ -119,13 +119,13 @@ }); } - if (!taskSyncManager.obsidianTask.hasDue()) { + if (!taskSyncManager.obsidianTask.hasSchedule()) { cardMenu.addItem((item) => { item.setTitle('Add Schedule'); item.setIcon('plus'); item.onClick((evt) => { taskSyncManager.taskCardStatus.scheduleStatus = 'editing'; - displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; + displaySchedule = taskSyncManager.obsidianTask.hasSchedule() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; }); }); } else { @@ -144,7 +144,7 @@ item.setIcon('plus'); item.onClick((evt) => { taskSyncManager.taskCardStatus.durationStatus = 'editing'; - displayDuration = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; + displayDuration = taskSyncManager.obsidianTask.hasSchedule() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; }); }); } else { @@ -208,7 +208,7 @@ cardMenu.showAtPosition({ x: event.clientX, y: event.clientY }); } - let displayDue: boolean = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; + let displaySchedule: boolean = taskSyncManager.obsidianTask.hasSchedule() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; let displayDuration: boolean = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; @@ -230,7 +230,7 @@ {#if descriptionProgress[1] * descriptionProgress[0] > 0 && !task.completed } {/if} - +
    @@ -261,9 +261,9 @@
    - + - {#if displayDue || displayDuration} + {#if displaySchedule || displayDuration}
    {/if} diff --git a/tests/suggester.test.ts b/tests/suggester.test.ts index 67f6749..198d7bb 100644 --- a/tests/suggester.test.ts +++ b/tests/suggester.test.ts @@ -58,7 +58,7 @@ describe('AttributeSuggester', () => { it('gets schedule suggestions', () => { const lineText = '{{ schedule: t }}'; const cursorPos = 9; - const suggestions = suggester.getDueSuggestions(lineText, cursorPos); + const suggestions = suggester.getScheduleSuggestions(lineText, cursorPos); expect(suggestions).toHaveLength(4); // Assuming 4 schedule suggestions are returned }); @@ -72,7 +72,7 @@ describe('AttributeSuggester', () => { it('wont get priority suggestions if cursor is not at the correct position', () => { const lineText = '{{ schedule: '; const cursorPos = 6; - const suggestions = suggester.getDueSuggestions(lineText, cursorPos); + const suggestions = suggester.getScheduleSuggestions(lineText, cursorPos); expect(suggestions).toHaveLength(0); }); From d3e4255fe6efbb93c0457e7ba6269d63ac7df6c4 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Mon, 30 Oct 2023 22:05:31 -0700 Subject: [PATCH 11/20] =?UTF-8?q?=E2=9C=A8=20add=20due=20and=20schedule?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/taskModule/task.ts | 10 + src/taskModule/taskParser.ts | 5 + src/taskModule/taskSyncManager.ts | 2 + src/ui/Due.svelte | 406 ++++++++++++++++++++++++++++++ src/ui/Schedule.svelte | 18 +- src/ui/TaskCard.svelte | 2 + 6 files changed, 434 insertions(+), 9 deletions(-) create mode 100644 src/ui/Due.svelte diff --git a/src/taskModule/task.ts b/src/taskModule/task.ts index 0a1b8f6..cb9ea6f 100644 --- a/src/taskModule/task.ts +++ b/src/taskModule/task.ts @@ -42,6 +42,7 @@ export interface TaskProperties { children: TaskProperties[] | ObsidianTask[]; schedule?: ScheduleDate | null; + due?: ScheduleDate | null; duration?: Duration | null; metadata?: { taskDisplayParams?: TaskDisplayParams | null; @@ -65,6 +66,7 @@ export class ObsidianTask implements TaskProperties { public children: TaskProperties[] | ObsidianTask[]; public schedule?: ScheduleDate | null; + public due?: ScheduleDate | null; public duration?: Duration | null; public metadata?: { @@ -86,6 +88,7 @@ export class ObsidianTask implements TaskProperties { this.parent = props?.parent || null; this.children = props?.children || []; this.schedule = props?.schedule || null; + this.due = props?.due || null; this.duration = props?.duration || null; this.metadata = props?.metadata || {}; } @@ -104,6 +107,7 @@ export class ObsidianTask implements TaskProperties { parent: this.parent, children: this.children, schedule: this.schedule, + due: this.due, duration: this.duration, metadata: this.metadata, }); @@ -139,6 +143,12 @@ export class ObsidianTask implements TaskProperties { return !!this.schedule.string.trim(); } + hasDue(): boolean { + if (!this.due) return false; + // return if the due string is not empty + return !!this.due.string.trim(); + } + hasDuration(): boolean { if (!this.duration) return false; // return if the duration string is not empty = hours and minutes all zero diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index ebabffb..151bcf8 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -88,6 +88,7 @@ export class TaskParser { task.parent = attributes.parent || null; task.children = attributes.children || []; task.schedule = attributes.schedule || null; + task.due = attributes.due || null; task.duration = attributes.duration || null; task.metadata = attributes.metadata || {}; @@ -284,6 +285,9 @@ export class TaskParser { case 'schedule': task.schedule = tryParseAttribute('schedule', this.parseSchedule.bind(this), attributeValue, 'other'); break; + case 'due': + task.due = tryParseAttribute('due', this.parseSchedule.bind(this), attributeValue, 'other'); + break; case 'duration': task.duration = tryParseAttribute('duration', this.parseDuration.bind(this), attributeValue, 'other'); break; @@ -374,6 +378,7 @@ export class TaskParser { task.order = parseJSONAttribute(metadata['order'], 'order', 0); task.project = parseJSONAttribute(metadata['project'], 'project', null); task.schedule = parseJSONAttribute(metadata['schedule'], 'schedule', null); + task.due = parseJSONAttribute(metadata['due'], 'due', null); task.duration = parseJSONAttribute(metadata['duration'], 'duration', null); task.metadata = parseJSONAttribute(metadata['metadata'], 'metadata', {}); diff --git a/src/taskModule/taskSyncManager.ts b/src/taskModule/taskSyncManager.ts index f387b3d..0b6f327 100644 --- a/src/taskModule/taskSyncManager.ts +++ b/src/taskModule/taskSyncManager.ts @@ -8,6 +8,7 @@ type TaskCardStatus = { descriptionStatus: 'editing' | 'done'; projectStatus: 'selecting' | 'done'; scheduleStatus: 'editing' | 'done'; + dueStatus: 'editing' | 'done'; durationStatus: 'editing' | 'done'; }; @@ -47,6 +48,7 @@ export class ObsidianTaskSyncManager implements ObsidianTaskSyncProps { descriptionStatus: 'done', projectStatus: 'done', scheduleStatus: 'done', + dueStatus: 'done', durationStatus: 'done', }; this.taskItemEl = props?.taskItemEl || null; diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte new file mode 100644 index 0000000..4353c57 --- /dev/null +++ b/src/ui/Due.svelte @@ -0,0 +1,406 @@ + + +{#if displayDue} +
    +
    + + + +
    + {#if taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} + adjustWidthForInput(inputElement)} + bind:value={dueString} + bind:this={inputElement} + class="task-card-due" + /> + {:else} +
    +
    + {dueDisplay} +
    +
    + {/if} +
    +{/if} + + \ No newline at end of file diff --git a/src/ui/Schedule.svelte b/src/ui/Schedule.svelte index b447652..5dfdc26 100644 --- a/src/ui/Schedule.svelte +++ b/src/ui/Schedule.svelte @@ -142,15 +142,15 @@ // upcoming and ongoing - function isUpcoming(schedule: ScheduleDate, currTime: moment.Moment, minuteGap: number = 15): boolean { - if (!schedule || !schedule.date || !schedule.time) { return false; } - // convert schedule to moment - const scheduleMoment = convertScheduleToMoment(schedule); - // calculate the time gap - const timeGap = moment.duration(scheduleMoment.diff(currTime)).asMinutes(); - // check if the time gap is within the specified range - return timeGap > 0 && timeGap < minuteGap; - } + // function isUpcoming(schedule: ScheduleDate, currTime: moment.Moment, minuteGap: number = 15): boolean { + // if (!schedule || !schedule.date || !schedule.time) { return false; } + // // convert schedule to moment + // const scheduleMoment = convertScheduleToMoment(schedule); + // // calculate the time gap + // const timeGap = moment.duration(scheduleMoment.diff(currTime)).asMinutes(); + // // check if the time gap is within the specified range + // return timeGap > 0 && timeGap < minuteGap; + // } enum TaskScheduleStatus { Upcoming = "upcoming", diff --git a/src/ui/TaskCard.svelte b/src/ui/TaskCard.svelte index a0138b6..6ab393e 100644 --- a/src/ui/TaskCard.svelte +++ b/src/ui/TaskCard.svelte @@ -19,6 +19,7 @@ import CircularProgressBar from '../components/CircularProgressBar.svelte'; import Duration from './Duration.svelte'; import SyncLogos from './SyncLogos.svelte'; + import Due from './Due.svelte'; export let taskSyncManager: ObsidianTaskSyncManager; export let plugin: TaskCardPlugin; @@ -261,6 +262,7 @@
    + {#if displaySchedule || displayDuration} From 27bd9a23fd328b646bffcabc4047f7ae16fba480 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Wed, 8 Nov 2023 17:15:56 -0800 Subject: [PATCH 12/20] =?UTF-8?q?=E2=9C=A8=20bug=20fixes=20+=20adjust=20du?= =?UTF-8?q?e=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/api/googleCalendarAPI/calendarAPI.ts | 4 +- src/components/icons/AlertTriangle.svelte | 14 +++ src/components/icons/CalendarCheck.svelte | 17 ++++ src/renderer/TaskCardRenderer.ts | 1 + src/taskModule/taskSyncManager.ts | 1 + src/ui/Due.svelte | 114 +++++++++++++--------- src/ui/Duration.svelte | 3 +- src/ui/Schedule.svelte | 1 + src/ui/TaskCard.svelte | 20 ++++ styles.css | 2 +- 10 files changed, 127 insertions(+), 50 deletions(-) create mode 100644 src/components/icons/AlertTriangle.svelte create mode 100644 src/components/icons/CalendarCheck.svelte diff --git a/src/api/googleCalendarAPI/calendarAPI.ts b/src/api/googleCalendarAPI/calendarAPI.ts index fa53007..7cb1754 100644 --- a/src/api/googleCalendarAPI/calendarAPI.ts +++ b/src/api/googleCalendarAPI/calendarAPI.ts @@ -38,6 +38,7 @@ export class GoogleCalendarAPI { this.calendars = await this.listCalendars(); } catch (e) { logger.error(`Failed to list calendars: ${e}`); + return; } SettingStore.subscribe((settings) => { const defaultCalendarId = settings.syncSettings.googleSyncSetting.defaultCalendarId; @@ -47,9 +48,6 @@ export class GoogleCalendarAPI { this.googleSyncSetting = settings.syncSettings.googleSyncSetting; }); - // DEBUG - this.defaultCalendar = this.calendars[1]; - // logger.debug(`defaultCalendar: ${JSON.stringify(this.defaultCalendar)}`); } async handleLocalTaskCreation(event: TaskChangeEvent): Promise { diff --git a/src/components/icons/AlertTriangle.svelte b/src/components/icons/AlertTriangle.svelte new file mode 100644 index 0000000..953a426 --- /dev/null +++ b/src/components/icons/AlertTriangle.svelte @@ -0,0 +1,14 @@ + + + + diff --git a/src/components/icons/CalendarCheck.svelte b/src/components/icons/CalendarCheck.svelte new file mode 100644 index 0000000..aa3109f --- /dev/null +++ b/src/components/icons/CalendarCheck.svelte @@ -0,0 +1,17 @@ + + + + + diff --git a/src/renderer/TaskCardRenderer.ts b/src/renderer/TaskCardRenderer.ts index 46d2f9d..f7d4a3e 100644 --- a/src/renderer/TaskCardRenderer.ts +++ b/src/renderer/TaskCardRenderer.ts @@ -98,6 +98,7 @@ descriptionStatus: 'done', projectStatus: 'done', scheduleStatus: 'done', + dueStatus: 'done', durationStatus: 'done' }, markdownTask: null, diff --git a/src/taskModule/taskSyncManager.ts b/src/taskModule/taskSyncManager.ts index 0b6f327..29618b8 100644 --- a/src/taskModule/taskSyncManager.ts +++ b/src/taskModule/taskSyncManager.ts @@ -128,6 +128,7 @@ export class ObsidianTaskSyncManager implements ObsidianTaskSyncProps { descriptionStatus: ['editing', 'done'], projectStatus: ['selecting', 'done'], scheduleStatus: ['editing', 'done'], + dueStatus: ['editing', 'done'], durationStatus: ['editing', 'done'] }; return allowedStatuses[key].includes(status); diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index 4353c57..b3f33b8 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -7,7 +7,7 @@ import { tick } from "svelte"; import { TaskDisplayParams, TaskDisplayMode } from "../renderer/postProcessor"; import { Notice } from "obsidian"; - import CalendarClock from "../components/icons/CalendarClock.svelte"; + import AlertTriangle from "../components/icons/AlertTriangle.svelte"; import moment from "moment"; export let taskSyncManager: ObsidianTaskSyncManager; @@ -85,65 +85,88 @@ } } - // function updateDueDisplay(): string { - // if (!due) { - // dueDisplay = ''; - // return dueDisplay; - // } - // let datePart = displayDate(due.date); - // let timePart = displayTime(due.time); - // dueDisplay = timePart ? `${datePart}, ${timePart}` : datePart; - // return dueDisplay; - // } - -function updateDueDisplay(): string { + function updateDueDisplay(mode = 'relative'): string { if (!due || !due.date) { dueDisplay = ''; return dueDisplay; } - // Convert to Moment object, considering cases where due.time is null or '' - logger.debug(`${JSON.stringify(due)}`); const dueDateTime = due.time ? moment(`${due.date} ${due.time}`) : moment(`${due.date}`); const now = moment(); - // Calculate differences - const yearsDiff = dueDateTime.diff(now, 'years'); - const monthsDiff = dueDateTime.diff(now, 'months'); - const weeksDiff = dueDateTime.diff(now, 'weeks'); - const daysDiff = dueDateTime.diff(now, 'days'); - const hoursDiff = dueDateTime.diff(now, 'hours'); - - // Choose display format based on the difference - if (yearsDiff > 1) { - dueDisplay = `${dueDateTime.year()}`; - } else if (yearsDiff === 1) { - dueDisplay = `${dueDateTime.format('MMM, YYYY')}`; - } else if (monthsDiff > 0) { - dueDisplay = `${dueDateTime.format('MMM DD')}`; - } else if (weeksDiff > 0) { - dueDisplay = `next ${dueDateTime.format('ddd, MMM DD')}`; - } else if (daysDiff > 1 || (daysDiff === 1 && hoursDiff >= 24)) { - // If due.time is null or empty, just show the day - if (!due.time) { - dueDisplay = `${dueDateTime.format('ddd')}`; + const diff = { + years: dueDateTime.diff(now, 'years', true), + months: dueDateTime.diff(now, 'months', true), + weeks: dueDateTime.diff(now, 'weeks', true), + days: dueDateTime.diff(now, 'days', true), + hours: dueDateTime.diff(now, 'hours', true), + minutes: dueDateTime.diff(now, 'minutes', true) + }; + + // If mode is 'absolute', use the existing logic to display a time point. + if (mode === 'absolute') { + + // Choose display format based on the difference + if (diff.years > 1) { + dueDisplay = `${dueDateTime.year()}`; + } else if (diff.years === 1) { + dueDisplay = `${dueDateTime.format('MMM, YYYY')}`; + } else if (diff.months > 0) { + dueDisplay = `${dueDateTime.format('MMM DD')}`; + } else if (diff.weeks > 0) { + dueDisplay = `next ${dueDateTime.format('ddd, MMM DD')}`; + } else if (diff.days > 1 || (diff.days === 1 && diff.hours >= 24)) { + // If due.time is null or empty, just show the day + if (!due.time) { + dueDisplay = `${dueDateTime.format('ddd')}`; + } + dueDisplay = `${dueDateTime.format('ddd, MMM DD, hA')}`; + } else if (diff.days === 1) { + dueDisplay = "Tomorrow"; + } else { + // If due.time is null or empty, return "Today" + if (!due.time) { + dueDisplay = "Today"; + } + dueDisplay = `${dueDateTime.format('h:mmA')}`; + } + return dueDisplay; + } + + // If mode is 'relative', calculate and display the time until the deadline. + else if (mode === 'relative') { + // Logic to determine which unit to display based on the difference. + // Always round down since we want to display until the deadline is reached. + // detect pass due + let prefix = ''; + if (dueDateTime.isBefore(now)) { + prefix = '- '; + // make diff to be absolute + diff.years = -diff.years; + diff.months = -diff.months; + diff.weeks = -diff.weeks; + diff.days = -diff.days; + diff.hours = -diff.hours; + diff.minutes = -diff.minutes; } - dueDisplay = `${dueDateTime.format('ddd, MMM DD, hA')}`; - } else if (daysDiff === 1) { - dueDisplay = "Tomorrow"; - } else { - // If due.time is null or empty, return "Today" - if (!due.time) { - dueDisplay = "Today"; + if (Math.floor(diff.years) > 0) { + dueDisplay = `${prefix}${Math.floor(diff.years)} year${Math.floor(diff.years) > 1 ? 's' : ''}`; + } else if (Math.floor(diff.months) > 0) { + dueDisplay = `${prefix}${Math.floor(diff.months)} month${Math.floor(diff.months) > 1 ? 's' : ''}`; + } else if (Math.floor(diff.days) > 0) { + dueDisplay = `${prefix}${Math.floor(diff.days)} day${Math.floor(diff.days) > 1 ? 's' : ''}`; + } else if (Math.floor(diff.hours) > 0) { + dueDisplay = `${prefix}${Math.floor(diff.hours)} hour${Math.floor(diff.hours) > 1 ? 's' : ''}`; + } else { + dueDisplay = `${prefix}${Math.floor(diff.minutes)} minute${Math.floor(diff.minutes) > 1 ? 's' : ''}`; } - dueDisplay = `Today, ${dueDateTime.format('h:mmA')}`; } + return dueDisplay; } - // Action function to focus and select the input content function focusAndSelect(node: HTMLInputElement) { // Focus on the input element @@ -253,7 +276,7 @@ function updateDueDisplay(): string { >
    - +
    {#if taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'} @@ -318,6 +341,7 @@ function updateDueDisplay(): string { display: flex; border-radius: 2em; overflow: hidden; + margin: 0 2px; font-size: var(--tag-size); border: var(--border-width) solid var(--text-accent); } diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index 93bda76..9f54f99 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -14,7 +14,7 @@ export let displayDuration: boolean; let duration: Duration | null; duration = taskSyncManager.obsidianTask.hasDuration() ? taskSyncManager.obsidianTask.duration : null; - + // TODO: distinguish due and schedule and duration; function customDurationHumanizer(duration: Duration) { if (duration.hours === 0) { @@ -197,6 +197,7 @@ function parseDurationInput(input: string): { hours: number, minutes: number } | display: flex; border-radius: 2em; overflow: hidden; + margin: 0 2px; font-size: var(--tag-size); border: var(--border-width) solid var(--text-accent); } diff --git a/src/ui/Schedule.svelte b/src/ui/Schedule.svelte index 5dfdc26..6e6183b 100644 --- a/src/ui/Schedule.svelte +++ b/src/ui/Schedule.svelte @@ -270,6 +270,7 @@ display: flex; border-radius: 2em; overflow: hidden; + margin: 0 2px; font-size: var(--tag-size); border: var(--border-width) solid var(--text-accent); } diff --git a/src/ui/TaskCard.svelte b/src/ui/TaskCard.svelte index 6ab393e..e45284d 100644 --- a/src/ui/TaskCard.svelte +++ b/src/ui/TaskCard.svelte @@ -139,6 +139,25 @@ }); } + if (!taskSyncManager.obsidianTask.hasDue()) { + cardMenu.addItem((item) => { + item.setTitle('Add Due'); + item.setIcon('plus'); + item.onClick((evt) => { + taskSyncManager.taskCardStatus.dueStatus = 'editing'; + displayDue = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'; + }); + }); + } else { + cardMenu.addItem((item) => { + item.setTitle('Delete Schedule'); + item.setIcon('trash'); + item.onClick((evt) => { + taskSyncManager.updateObsidianTaskAttribute('schedule', null); + }); + }); + } + if (!taskSyncManager.obsidianTask.hasDuration()) { cardMenu.addItem((item) => { item.setTitle('Add Duration'); @@ -211,6 +230,7 @@ let displaySchedule: boolean = taskSyncManager.obsidianTask.hasSchedule() || taskSyncManager.getTaskCardStatus('scheduleStatus') === 'editing'; let displayDuration: boolean = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; + let displayDue: boolean = taskSyncManager.obsidianTask.hasDue() || taskSyncManager.getTaskCardStatus('dueStatus') === 'editing'; diff --git a/styles.css b/styles.css index b277380..7bb79a8 100644 --- a/styles.css +++ b/styles.css @@ -140,7 +140,7 @@ .task-card-attribute-separator { flex-shrink: 0; /* Prevents shrinking */ - margin: 0 4px; + margin: 0 3px 0 2px; margin-top: 3px; border-left: 1px solid var(--interactive-hover); height: 13px; From 6e3a7de50ef08cd9f613c6df0578df38f622cbe4 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Wed, 8 Nov 2023 22:49:25 -0800 Subject: [PATCH 13/20] integrate due and schedule to static cards --- package-lock.json | 32 +++--- package.json | 2 +- src/api/googleCalendarAPI/calendarAPI.ts | 1 + .../description/descriptionParser.ts | 3 +- src/taskModule/taskParser.ts | 49 ++++++++- src/ui/Content.svelte | 21 ++-- src/ui/Description.svelte | 82 ++++++++------ src/ui/Due.svelte | 55 +++++----- src/ui/Duration.svelte | 32 ++++-- src/ui/Schedule.svelte | 60 ++++++----- src/ui/StaticTaskCard.svelte | 100 +++++++----------- src/ui/TaskCard.svelte | 11 +- src/utils/markdownToHTML.ts | 15 +++ 13 files changed, 273 insertions(+), 190 deletions(-) create mode 100644 src/utils/markdownToHTML.ts diff --git a/package-lock.json b/package-lock.json index b4aa73c..d928634 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,11 +13,11 @@ "humanize-duration": "^3.30.0", "humanized-duration": "^0.0.1", "lucide-svelte": "^0.268.0", - "marked": "^6.0.0", "obsidian": "latest", "obsidian-dataview": "^0.5.56", "parse-duration": "^1.1.0", "runtypes": "^6.7.0", + "showdown": "^2.1.0", "sugar": "^2.0.6", "svelte": "^4.1.1", "typescript": "^5.1.6", @@ -3788,6 +3788,14 @@ "node": ">= 0.8" } }, + "node_modules/commander": { + "version": "9.5.0", + "resolved": "https://registry.npmmirror.com/commander/-/commander-9.5.0.tgz", + "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", + "engines": { + "node": "^12.20.0 || >=14" + } + }, "node_modules/component-emitter": { "version": "1.3.0", "resolved": "https://registry.npmmirror.com/component-emitter/-/component-emitter-1.3.0.tgz", @@ -6675,17 +6683,6 @@ "node": ">=0.10.0" } }, - "node_modules/marked": { - "version": "6.0.0", - "resolved": "https://registry.npmmirror.com/marked/-/marked-6.0.0.tgz", - "integrity": "sha512-7E3m/xIlymrFL5gWswIT4CheIE3fDeh51NV09M4x8iOc7NDYlyERcQMLAIHcSlrvwliwbPQ4OGD+MpPSYiQcqw==", - "bin": { - "marked": "bin/marked.js" - }, - "engines": { - "node": ">= 16" - } - }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmmirror.com/mdn-data/-/mdn-data-2.0.30.tgz", @@ -8143,6 +8140,17 @@ "node": ">=8" } }, + "node_modules/showdown": { + "version": "2.1.0", + "resolved": "https://registry.npmmirror.com/showdown/-/showdown-2.1.0.tgz", + "integrity": "sha512-/6NVYu4U819R2pUIk79n67SYgJHWCce0a5xTP979WbNp0FL9MN1I1QK662IDU1b6JzKTvmhgI7T7JYIxBi3kMQ==", + "dependencies": { + "commander": "^9.0.0" + }, + "bin": { + "showdown": "bin/showdown.js" + } + }, "node_modules/side-channel": { "version": "1.0.4", "resolved": "https://registry.npmmirror.com/side-channel/-/side-channel-1.0.4.tgz", diff --git a/package.json b/package.json index 5c390a2..d254009 100644 --- a/package.json +++ b/package.json @@ -53,11 +53,11 @@ "humanize-duration": "^3.30.0", "humanized-duration": "^0.0.1", "lucide-svelte": "^0.268.0", - "marked": "^6.0.0", "obsidian": "latest", "obsidian-dataview": "^0.5.56", "parse-duration": "^1.1.0", "runtypes": "^6.7.0", + "showdown": "^2.1.0", "sugar": "^2.0.6", "svelte": "^4.1.1", "typescript": "^5.1.6", diff --git a/src/api/googleCalendarAPI/calendarAPI.ts b/src/api/googleCalendarAPI/calendarAPI.ts index 7cb1754..9f46d71 100644 --- a/src/api/googleCalendarAPI/calendarAPI.ts +++ b/src/api/googleCalendarAPI/calendarAPI.ts @@ -64,6 +64,7 @@ export class GoogleCalendarAPI { // 1. intrinsic filter: task without schedule date won't be created if (!event.currentState.schedule?.date) return false; // Optional chaining is used here // 2. setting based filter: if there's filter project or tag, check if the task is in the project or tag + if (!this.googleSyncSetting) return false; if (this.googleSyncSetting.doesNeedFilters && this.googleSyncSetting.filterProject) { if (this.googleSyncSetting.filterProject !== event.currentState.project?.id) return false; } diff --git a/src/taskModule/description/descriptionParser.ts b/src/taskModule/description/descriptionParser.ts index 657228e..0425b28 100644 --- a/src/taskModule/description/descriptionParser.ts +++ b/src/taskModule/description/descriptionParser.ts @@ -44,8 +44,7 @@ export class DescriptionParser { throw new Error(`Failed to convert HTML to Markdown: ${error.message}`); } } - - return descriptionMarkdown.trim(); + return descriptionMarkdown; } static progressOfDescription(description: string): [number, number] { diff --git a/src/taskModule/taskParser.ts b/src/taskModule/taskParser.ts index 151bcf8..c459658 100644 --- a/src/taskModule/taskParser.ts +++ b/src/taskModule/taskParser.ts @@ -77,7 +77,7 @@ export class TaskParser { const task = new ObsidianTask(); const attributes = parseAttributes.bind(this)(); if (attributes === null) { return task; } - + task.id = attributes.id || ''; task.priority = attributes.priority || '1'; task.description = (attributes.description || '') + (DescriptionParser.parseDescriptionFromTaskEl(taskEl) || ''); @@ -322,10 +322,51 @@ export class TaskParser { parseFormattedTaskMarkdown(taskMarkdown: string): ObsidianTask { const task: ObsidianTask = new ObsidianTask(); - // if taskMarkdown is multi-line, split it + // Utility function to convert leading tabs in a line to spaces + const convertLeadingTabsToSpaces = (line: string): string => { + let result = ""; + let index = 0; + + while (index < line.length) { + if (line[index] === '\t') { + result += " "; // Replace tab with 4 spaces + } else if (line[index] === ' ') { + result += line[index]; + } else { + // Once we encounter a non-space, non-tab character, break + break; + } + index++; + } + + // Append the remainder of the line + result += line.slice(index); + return result; + }; + + // Utility function to check if all lines start with a space + const allLinesStartWithSpace = (lines: string[]): boolean => { + return lines.every(line => line.startsWith(" ")); + }; + + // Utility function to remove the leading space from all lines + const removeLeadingSpace = (lines: string[]): string[] => { + return lines.map(line => line.startsWith(" ") ? line.slice(1) : line); + }; + if (taskMarkdown.includes('\n')) { - const lines = taskMarkdown.split('\n'); - task.description = lines.slice(1).join('\n'); // From the second line to the last line, joined by '\n' + // process multi-line task - has description + let lines = taskMarkdown.split('\n'); + let descLines = lines.slice(1); + // Convert tabs to spaces + descLines = descLines.map(convertLeadingTabsToSpaces); + + // Iteratively remove indentation + while (allLinesStartWithSpace(descLines)) { + descLines = removeLeadingSpace(descLines); + } + // logger.debug(`Multi-line task: ${descLines.join('\n')}`); + task.description = descLines.join('\n'); taskMarkdown = lines[0]; // The first line } diff --git a/src/ui/Content.svelte b/src/ui/Content.svelte index d3872b7..77534f8 100644 --- a/src/ui/Content.svelte +++ b/src/ui/Content.svelte @@ -3,8 +3,13 @@ import { ObsidianTaskSyncManager } from '../taskModule/taskSyncManager'; import { TaskDisplayMode } from '../renderer/postProcessor'; import { logger } from '../utils/log'; - export let taskSyncManager: ObsidianTaskSyncManager; - let content: string = taskSyncManager.obsidianTask.content; + import { ObsidianTask } from '../taskModule/task'; + + export let interactive: boolean = true; + export let taskSyncManager: ObsidianTaskSyncManager = undefined; + export let taskItem: ObsidianTask = undefined; + + let content: string = interactive ? taskSyncManager.obsidianTask.content : taskItem.content; let isEditing = false; let inputElement: HTMLInputElement; @@ -56,17 +61,17 @@ {#if isEditing} {:else}
    {content} @@ -75,11 +80,11 @@ \ No newline at end of file + /* Add any other styles you need */ + From 91e25820bfe5eb9ffff6996bbf1f3725353cdb67 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Thu, 9 Nov 2023 14:15:06 -0800 Subject: [PATCH 16/20] =?UTF-8?q?=E2=9C=A8=20adjust=20style=20and=20visibi?= =?UTF-8?q?lity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/renderer/StaticTaskListRenderer.ts | 5 ++ src/ui/StaticTaskCard.svelte | 2 + src/ui/StaticTaskMatrix.svelte | 100 +++++++++++++++++++------ 3 files changed, 86 insertions(+), 21 deletions(-) diff --git a/src/renderer/StaticTaskListRenderer.ts b/src/renderer/StaticTaskListRenderer.ts index e39efc4..76c52e4 100644 --- a/src/renderer/StaticTaskListRenderer.ts +++ b/src/renderer/StaticTaskListRenderer.ts @@ -16,6 +16,11 @@ export interface CodeBlockProcessor { (source: string, el: HTMLElement, ctx: MarkdownPostProcessorContext): void | Promise; } +export enum StaticTaskCardDisplayMode { + normal, + compact, + } + export class StaticTaskListRenderManager { plugin: TaskCardPlugin; constructor(plugin: TaskCardPlugin) { diff --git a/src/ui/StaticTaskCard.svelte b/src/ui/StaticTaskCard.svelte index e3c3bd0..772f5d6 100644 --- a/src/ui/StaticTaskCard.svelte +++ b/src/ui/StaticTaskCard.svelte @@ -21,10 +21,12 @@ // Markdown to HTML import { markdownToHTML } from '../utils/markdownToHTML'; + import { StaticTaskCardDisplayMode } from '../renderer/StaticTaskListRenderer'; export let taskItem: PositionedObsidianTask; export let plugin: TaskCardPlugin; + export let staticTaskCardDisplayMode: StaticTaskCardDisplayMode = StaticTaskCardDisplayMode.normal; let taskDisplayParams: TaskDisplayParams = { mode: 'single-line' }; let task = taskItem; diff --git a/src/ui/StaticTaskMatrix.svelte b/src/ui/StaticTaskMatrix.svelte index 3c798bb..36dad9a 100644 --- a/src/ui/StaticTaskMatrix.svelte +++ b/src/ui/StaticTaskMatrix.svelte @@ -16,18 +16,30 @@ querySyncManager.toEditMode(); } - // Define a function to categorize tasks + let counts = { + do: 0, + plan: 0, + delegate: 0, + delete: 0 + }; + + $: if(taskList) { + counts = taskList.reduce((acc, task) => { + const category = categorizeTasks(task); + acc[category] = (acc[category] || 0) + 1; + return acc; + }, counts); + } + function categorizeTasks(task: PositionedObsidianTask): string { - // For demo purposes, you can design how to categorize tasks here - // For example, you can use task properties like 'important' and 'urgent' if (task.priority > 1 && task.due) { - return "important-urgent"; + return "do"; // was "important-urgent" } else if (task.priority > 1 && !task.due) { - return "important-not-urgent"; + return "plan"; // was "important-not-urgent" } else if (!(task.priority > 1) && task.due) { - return "not-important-urgent"; + return "delegate"; // was "not-important-urgent" } else { - return "not-important-not-urgent"; + return "delete"; // was "not-important-not-urgent" } } @@ -40,48 +52,48 @@
    {:else if taskList.length > 0}
    -
    -

    Important and Urgent

    +
    +
    Do ({counts.do})
      {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "important-urgent"} + {#if categorizeTasks(taskItem) === "do"} {/if} {/each}
    -
    -

    Important and Not Urgent

    +
    +
    Plan ({counts.plan})
      {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "important-not-urgent"} + {#if categorizeTasks(taskItem) === "plan"} {/if} {/each}
    -
    -

    Not Important and Urgent

    +
    +
    Delegate ({counts.delegate})
      {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "not-important-urgent"} + {#if categorizeTasks(taskItem) === "delegate"} {/if} {/each}
    -
    -

    Not Important and Not Urgent

    +
    +
    Delete ({counts.delete})
      {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "not-important-not-urgent"} + {#if categorizeTasks(taskItem) === "delete"} {/if} {/each} @@ -103,6 +115,11 @@ From 6327e834f11f1805ed4cb6290f6a7e1ca0827436 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Thu, 7 Mar 2024 22:54:23 -0800 Subject: [PATCH 17/20] =?UTF-8?q?=F0=9F=8E=A8=20display=20fixes=20and=20im?= =?UTF-8?q?provements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODOs.md | 3 + src/components/LinearProgressBar.svelte | 84 +++++++ src/query/cache.ts | 1 + src/query/querySyncManager.ts | 2 + src/renderer/queryAndTaskListSvelteAdapter.ts | 19 +- src/settings.ts | 24 +- src/settings/displaySettings.ts | 21 ++ src/ui/Due.svelte | 2 + src/ui/Duration.svelte | 1 + src/ui/Labels.svelte | 1 + src/ui/QueryDisplay.svelte | 83 +++++++ src/ui/QueryEditor.svelte | 7 +- src/ui/Schedule.svelte | 1 + src/ui/StaticTaskCard.svelte | 22 +- src/ui/StaticTaskItem.svelte | 11 +- src/ui/StaticTaskList.svelte | 64 +---- src/ui/StaticTaskMatrix.svelte | 235 ++++++++++-------- .../selections/FixedOptionsMultiSelect.svelte | 126 ++++++++++ src/ui/selections/FixedOptionsSelect.svelte | 30 +-- 19 files changed, 507 insertions(+), 230 deletions(-) create mode 100644 src/components/LinearProgressBar.svelte create mode 100644 src/ui/QueryDisplay.svelte create mode 100644 src/ui/selections/FixedOptionsMultiSelect.svelte diff --git a/TODOs.md b/TODOs.md index e69de29..8a74cd8 100644 --- a/TODOs.md +++ b/TODOs.md @@ -0,0 +1,3 @@ + + +# Finish Eisenhower Matrix diff --git a/src/components/LinearProgressBar.svelte b/src/components/LinearProgressBar.svelte new file mode 100644 index 0000000..16c4f97 --- /dev/null +++ b/src/components/LinearProgressBar.svelte @@ -0,0 +1,84 @@ + + +
      +
      + + + + +
      + {#if showDigits} +
      + {value}/{max} +
      + {/if} +
      + + diff --git a/src/query/cache.ts b/src/query/cache.ts index 490b248..a41fc77 100644 --- a/src/query/cache.ts +++ b/src/query/cache.ts @@ -16,6 +16,7 @@ export interface MultipleAttributeTaskQuery { completedQuery?: boolean[]; scheduleDateTimeQuery?: [string, string]; filePathQuery?: string; + displayModeQuery?: string; } export interface TaskRow { diff --git a/src/query/querySyncManager.ts b/src/query/querySyncManager.ts index 9f11e59..56b836e 100644 --- a/src/query/querySyncManager.ts +++ b/src/query/querySyncManager.ts @@ -199,6 +199,8 @@ export class QuerySyncManager { async getFilteredTasks(): Promise { const filteredTasksProps = await this.plugin.cache.taskCache.queryTasks(this.taskQuery) const filteredTasks = filteredTasksProps.map((taskProps) => new PositionedObsidianTask(taskProps)) + console.log(filteredTasks) + console.log(this.taskQuery) return filteredTasks } } \ No newline at end of file diff --git a/src/renderer/queryAndTaskListSvelteAdapter.ts b/src/renderer/queryAndTaskListSvelteAdapter.ts index 49fe7c3..986522b 100644 --- a/src/renderer/queryAndTaskListSvelteAdapter.ts +++ b/src/renderer/queryAndTaskListSvelteAdapter.ts @@ -6,6 +6,7 @@ import { QuerySyncManager } from "../query/querySyncManager" import { MarkdownPostProcessorContext, MarkdownSectionInformation } from "obsidian" import { logger } from "../utils/log"; import StaticTaskMatrix from "../ui/StaticTaskMatrix.svelte"; +import QueryDisplay from "../ui/QueryDisplay.svelte"; export class QueryAndTaskListSvelteAdapter { @@ -58,14 +59,16 @@ export class QueryAndTaskListSvelteAdapter { } }) } else { - this.svelteComponent = new StaticTaskMatrix({ - target: this.codeBlockEl, - props: { - taskList: await this.querySyncManager.getFilteredTasks(), - plugin: this.plugin, - querySyncManager: this.querySyncManager - } - }) + this.svelteComponent = new QueryDisplay({ + target: this.codeBlockEl, + props: { + taskList: await this.querySyncManager.getFilteredTasks(), + plugin: this.plugin, + querySyncManager: this.querySyncManager, + displayMode: this.plugin.settings.displaySettings.queryDisplayMode, + } + }) + } } diff --git a/src/settings.ts b/src/settings.ts index 6b86dd3..58fbdc7 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -25,6 +25,7 @@ export interface TaskCardSettings { displaySettings: { defaultMode: string; upcomingMinutes: number; + queryDisplayMode: string; }; userMetadata: { projects: any; @@ -53,6 +54,7 @@ export const DefaultSettings: TaskCardSettings = { displaySettings: { defaultMode: 'single-line', upcomingMinutes: 15, + queryDisplayMode: 'line' }, userMetadata: { projects: {}, @@ -591,28 +593,6 @@ export class SettingsTab extends PluginSettingTab { } - // cardDisplaySettings() { - // new Setting(this.containerEl) - // .setName('Default Display Mode') - // .setDesc('The default display mode when creating a new task card.') - // .addDropdown((dropdown) => { - // dropdown - // .addOptions({ - // 'single-line': 'Preview Mode', - // 'multi-line': 'Detailed Mode' - // }) - // .setValue(this.plugin.settings.displaySettings.defaultMode) - // .onChange(async (value: string) => { - // await this.plugin.writeSettings( - // (old) => (old.displaySettings.defaultMode = value) - // ); - // logger.info(`Default display mode updated: ${value}`); - // new Notice(`[TaskCard] Default display mode updated: ${value}.`); - // }); - // }); - // } - - } export { GoogleSyncSetting }; diff --git a/src/settings/displaySettings.ts b/src/settings/displaySettings.ts index 7b825fd..c618dcc 100644 --- a/src/settings/displaySettings.ts +++ b/src/settings/displaySettings.ts @@ -59,4 +59,25 @@ export function cardDisplaySettings( }, 2000); // 2000 milliseconds = 2 seconds delay }); }); + + new Setting(containerEl) + .setName('Query Display Mode') + .setDesc('The default display mode when displaying a task query.') + .addDropdown((dropdown) => { + dropdown + .addOptions({ + 'list': 'List Mode', + 'matrix': 'Eisenhower Matrix Mode' + }) + .setValue(pluginSettings.displaySettings.queryDisplayMode) + .onChange(async (value: string) => { + await writeSettings( + (old) => (old.displaySettings.queryDisplayMode = value) + ); + logger.info(`Query display mode updated: ${value}`); + new Notice(`[TaskCard] Query display mode updated: ${value}.`); + } + ); + } + ); } \ No newline at end of file diff --git a/src/ui/Due.svelte b/src/ui/Due.svelte index 8b6921b..e90b929 100644 --- a/src/ui/Due.svelte +++ b/src/ui/Due.svelte @@ -343,10 +343,12 @@ align-items: center; display: flex; border-radius: 2em; + min-width: 2em; overflow: hidden; margin: 0 2px; font-size: var(--tag-size); border: var(--border-width) solid var(--text-accent); + mask-image: webkit-gradient(linear, left 90%, left bottom, from(rgba(0,0,0,1)), to(rgba(0,0,0,0))) } .task-card-due-container.ongoing { diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index f170ba8..b9c5574 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -206,6 +206,7 @@ function parseDurationInput(input: string): { hours: number, minutes: number } | align-items: center; display: flex; border-radius: 2em; + min-width: 2em; overflow: hidden; margin: 0 2px; font-size: var(--tag-size); diff --git a/src/ui/Labels.svelte b/src/ui/Labels.svelte index adc8ed0..bb23dc3 100644 --- a/src/ui/Labels.svelte +++ b/src/ui/Labels.svelte @@ -133,6 +133,7 @@ overflow: scroll; /* Truncates any labels that don't fit */ white-space: nowrap; /* Keeps labels on a single line */ align-items: center; + min-width: 2em; gap: 4px; flex-grow: 1; /* Make it take up all available space */ font-size: var(--font-ui-medium); diff --git a/src/ui/QueryDisplay.svelte b/src/ui/QueryDisplay.svelte new file mode 100644 index 0000000..09893c0 --- /dev/null +++ b/src/ui/QueryDisplay.svelte @@ -0,0 +1,83 @@ + + +
        + {#if !cacheInitialized} +
        +

        Task Card Query Failed

        +

        Tasks Not Fully Indexed. Please make sure that the dataview plugin is also enabled in Obsidian. This is necessary for this feature to work properly

        +
        + {:else if taskList.length > 0} + {#if displayMode === "list"} + + {:else if displayMode === "matrix"} + + {/if} + {:else} +
        +

        No Tasks Found

        +

        It looks like there are no tasks that match your filter.

        +
        + {/if} +
      +
      + TaskCard Query: {taskList.length} / {querySyncManager.plugin.cache.taskCache.getLength()} tasks. + + +
      + + diff --git a/src/ui/QueryEditor.svelte b/src/ui/QueryEditor.svelte index 4e9010d..6ce5c55 100644 --- a/src/ui/QueryEditor.svelte +++ b/src/ui/QueryEditor.svelte @@ -4,6 +4,7 @@ import { MultipleAttributeTaskQuery } from "../query/cache"; import { QuerySyncManager, TaskQueryOptions } from "../query/querySyncManager"; import { logger } from "../utils/log"; + import FixedOptionsMultiSelect from "./selections/FixedOptionsMultiSelect.svelte"; import FixedOptionsSelect from "./selections/FixedOptionsSelect.svelte"; import ProjectSelection from "./selections/ProjectSelection.svelte"; import TagSelect from "./selections/TagSelect.svelte"; @@ -144,7 +145,7 @@
        - - +
      • @@ -240,6 +242,7 @@
      • +
        diff --git a/src/ui/Schedule.svelte b/src/ui/Schedule.svelte index 6efa464..b4ac675 100644 --- a/src/ui/Schedule.svelte +++ b/src/ui/Schedule.svelte @@ -273,6 +273,7 @@ align-items: center; display: flex; border-radius: 2em; + min-width: 2em; overflow: hidden; margin: 0 2px; font-size: var(--tag-size); diff --git a/src/ui/StaticTaskCard.svelte b/src/ui/StaticTaskCard.svelte index 772f5d6..d8a77cb 100644 --- a/src/ui/StaticTaskCard.svelte +++ b/src/ui/StaticTaskCard.svelte @@ -21,12 +21,11 @@ // Markdown to HTML import { markdownToHTML } from '../utils/markdownToHTML'; - import { StaticTaskCardDisplayMode } from '../renderer/StaticTaskListRenderer'; export let taskItem: PositionedObsidianTask; export let plugin: TaskCardPlugin; - export let staticTaskCardDisplayMode: StaticTaskCardDisplayMode = StaticTaskCardDisplayMode.normal; + let taskDisplayParams: TaskDisplayParams = { mode: 'single-line' }; let task = taskItem; @@ -99,7 +98,6 @@ const displayDuration = task.hasDuration(); const displayDue = task.hasDue(); // const displayDescription = task.hasDescription(); - {#if taskDisplayParams.mode === 'single-line'} @@ -241,7 +239,6 @@ diff --git a/src/ui/StaticTaskList.svelte b/src/ui/StaticTaskList.svelte index 3777dbd..e7eddd2 100644 --- a/src/ui/StaticTaskList.svelte +++ b/src/ui/StaticTaskList.svelte @@ -2,73 +2,17 @@ import { ObsidianTask, PositionedObsidianTask } from "../taskModule/task"; import StaticTaskItem from './StaticTaskItem.svelte'; import TaskCardPlugin from ".."; - import { logger } from "../utils/log"; - import { QuerySyncManager } from "../query/querySyncManager"; export let taskList: PositionedObsidianTask[]; export let plugin: TaskCardPlugin; - export let querySyncManager: QuerySyncManager; - const cacheInitialized = plugin.cache.taskCache.status.initialized; - - function toEditMode() { - querySyncManager.toEditMode(); - } -
          - {#if !cacheInitialized} -
          -

          Task Card Query Failed

          -

          Tasks Not Fully Indexed. Please make sure that the dataview plugin is also enabled in Obsidian. This is necessary for this feature to work properly

          -
          - {:else if taskList.length > 0} - {#each taskList as taskItem} - - {/each} - {:else} -
          -

          No Tasks Found

          -

          It looks like there are no tasks that match your filter.

          -
          - {/if} -
        -
        - TaskCard Query: {taskList.length} / {querySyncManager.plugin.cache.taskCache.getLength()} tasks. - +
        + {#each taskList as taskItem} + + {/each}
        diff --git a/src/ui/StaticTaskMatrix.svelte b/src/ui/StaticTaskMatrix.svelte index 36dad9a..143b219 100644 --- a/src/ui/StaticTaskMatrix.svelte +++ b/src/ui/StaticTaskMatrix.svelte @@ -3,30 +3,33 @@ import StaticTaskItem from './StaticTaskItem.svelte'; import TaskCardPlugin from ".."; import { logger } from "../utils/log"; - import { QuerySyncManager } from "../query/querySyncManager"; - import StaticTaskCard from "./StaticTaskCard.svelte"; + import LinearProgressBar from "../components/LinearProgressBar.svelte"; export let taskList: PositionedObsidianTask[]; export let plugin: TaskCardPlugin; - export let querySyncManager: QuerySyncManager; - - const cacheInitialized = plugin.cache.taskCache.status.initialized; - - function toEditMode() { - querySyncManager.toEditMode(); - } let counts = { - do: 0, - plan: 0, - delegate: 0, - delete: 0 + do: { count: 0, completedTasks: 0 }, + plan: { count: 0, completedTasks: 0 }, + delegate: { count: 0, completedTasks: 0 }, + delete: { count: 0, completedTasks: 0 } }; $: if(taskList) { + // Reset counts object to start fresh on each reactive update + counts = { + do: { count: 0, completedTasks: 0 }, + plan: { count: 0, completedTasks: 0 }, + delegate: { count: 0, completedTasks: 0 }, + delete: { count: 0, completedTasks: 0 } + }; + counts = taskList.reduce((acc, task) => { const category = categorizeTasks(task); - acc[category] = (acc[category] || 0) + 1; + acc[category].count += 1; + if (task.completed) { + acc[category].completedTasks += 1; + } return acc; }, counts); } @@ -44,73 +47,77 @@ } -
          - {#if !cacheInitialized} -
          -

          Task Card Query Failed

          -

          Tasks Not Fully Indexed. Please make sure that the dataview plugin is also enabled in Obsidian. This is necessary for this feature to work properly

          -
          - {:else if taskList.length > 0} -
          -
          -
          Do ({counts.do})
          -
          -
            - {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "do"} - - {/if} - {/each} -
          +
          +
          +
          +
          +
          Do
          +
          +
          -
          -
          Plan ({counts.plan})
          -
          -
            - {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "plan"} - - {/if} - {/each} -
          -
          +
          +
            + {#each taskList as taskItem} + {#if categorizeTasks(taskItem) === "do"} + + {/if} + {/each} +
          -
          -
          Delegate ({counts.delegate})
          -
          -
            - {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "delegate"} - - {/if} - {/each} -
          +
          +
          +
          +
          Plan
          +
          +
          -
          -
          Delete ({counts.delete})
          -
          -
            - {#each taskList as taskItem} - {#if categorizeTasks(taskItem) === "delete"} - - {/if} - {/each} -
          +
          +
            + {#each taskList as taskItem} + {#if categorizeTasks(taskItem) === "plan"} + + {/if} + {/each} +
          +
          +
          +
          +
          +
          Delegate
          +
          +
          +
          +
            + {#each taskList as taskItem} + {#if categorizeTasks(taskItem) === "delegate"} + + {/if} + {/each} +
          +
          - {:else} -
          -

          No Tasks Found

          -

          It looks like there are no tasks that match your filter.

          +
          +
          +
          Delete
          +
          + +
          +
          +
          +
            + {#each taskList as taskItem} + {#if categorizeTasks(taskItem) === "delete"} + + {/if} + {/each} +
          +
          - {/if} -
        -
        - TaskCard Query: {taskList.length} / {querySyncManager.plugin.cache.taskCache.getLength()} tasks. - +
        @@ -122,9 +129,6 @@ /* Add CSS for the 2x2 grid layout */ .matrix-container { - display: grid; - grid-template-columns: 1fr 1fr; /* 2 columns with equal width */ - grid-template-rows: auto auto; /* Auto-size rows based on content */ gap: 10px; /* Adjust the gap as needed */ max-width: 100%; /* Set the maximum width to 100% */ overflow-x: hidden; /* Disable horizontal scrolling for the whole grid */ @@ -133,58 +137,73 @@ /* Add styles for each category */ .category { flex: 1 1 auto; - overflow: auto; background-color: transparent; /* Set a transparent background */ - padding: 0; /* Remove padding */ + padding: 10px; /* Add padding as needed */ + margin: 10px; /* Add margin as needed */ + border-radius: var(--radius-m); /* Add border radius as needed */ max-height: 50vh; /* Allow vertical scrolling within each category */ - overflow: hidden; + overflow-x: hidden; /* Disable horizontal scrolling for each category */ + overflow-y: auto; /* Enable vertical scrolling for each category */ + /* put element in the middle */ + display: flex; + flex-direction: column; + justify-content: center; } - .task-list { - width: 100%; - max-height: 250px; - overflow-x: hidden; - overflow-y: auto; + .category-head { + display: flex; + align-items: center; + justify-content: center; + /* some space between elements */ + gap: 10px; + margin-bottom: 0.25em; } .category-title { - font-size: 1em; /* Smaller size, adjust as needed */ - font-weight: bold; - margin-bottom: 0.5em; - padding: 0.25em; - /* Add more styles as needed for prettiness */ - } - - .button-menu { + font-size: 1.5em; /* Adjust the font size as needed */ + font-weight: bold; /* Add bold font weight */ display: flex; + justify-content: center; align-items: center; + } + + .category-count { + font-size: 1.0em; + font-weight: thin; + margin: 0.25em; + display: flex; justify-content: center; + align-items: center; } - .edit-button { - width: 40%; - color: var(--text-normal); - margin: 10px; + + /* Category-specific background and title colors for differentiation */ + .category.do { + background-color: rgba(var(--color-purple-rgb), 0.05); + border: 1px solid var(--color-purple); } - .error-page { - text-align: center; - font-size: 14px; - color: var(--text-muted); - margin: 20px; + .category.plan { + background-color: rgba(var(--color-cyan-rgb), 0.05); + border: 1px solid var(--color-cyan); } - .error-page h2 { - font-size: 24px; - margin-bottom: 10px; + .category.delegate { + background-color: rgba(var(--color-orange-rgb), 0.05); + border: 1px solid var(--color-orange); } - .error-page p { - margin-bottom: 20px; + .category.delete { + background-color: rgba(var(--mono-rgb-100), 0.05); + border: 1px solid var(--mono-rgb-0); } - .list-stats { - font-size: var(--font-ui-small); - color: var(--text-muted); + + .task-list { + width: 100%; + max-height: 250px; + overflow-x: hidden; + overflow-y: auto; } + diff --git a/src/ui/selections/FixedOptionsMultiSelect.svelte b/src/ui/selections/FixedOptionsMultiSelect.svelte new file mode 100644 index 0000000..e08e94a --- /dev/null +++ b/src/ui/selections/FixedOptionsMultiSelect.svelte @@ -0,0 +1,126 @@ + + + + + + + + +
        +
        +
        +
        {title}
        +
        {description}
        +
        +
        +
        + {#each choices as choice (choice.value)} + + {/each} +
        +
        +
        + + + + \ No newline at end of file diff --git a/src/ui/selections/FixedOptionsSelect.svelte b/src/ui/selections/FixedOptionsSelect.svelte index e08e94a..0fe5a8e 100644 --- a/src/ui/selections/FixedOptionsSelect.svelte +++ b/src/ui/selections/FixedOptionsSelect.svelte @@ -1,11 +1,8 @@ - - -
        @@ -53,9 +45,9 @@
        {#each choices as choice (choice.value)} {/each} From 1a416ba995b73811daed384f715aa9776552776c Mon Sep 17 00:00:00 2001 From: terryli710 Date: Fri, 8 Mar 2024 11:08:16 -0800 Subject: [PATCH 18/20] fix static task link display mode --- TODOs.md | 3 +++ src/ui/StaticTaskCard.svelte | 49 ++++++++++++++++++++++------------ src/ui/StaticTaskMatrix.svelte | 2 +- 3 files changed, 36 insertions(+), 18 deletions(-) diff --git a/TODOs.md b/TODOs.md index 8a74cd8..af895a4 100644 --- a/TODOs.md +++ b/TODOs.md @@ -1,3 +1,6 @@ # Finish Eisenhower Matrix +- Fix the subtask display in static task card. +- Fix the bugs in console. +- \ No newline at end of file diff --git a/src/ui/StaticTaskCard.svelte b/src/ui/StaticTaskCard.svelte index d8a77cb..400150c 100644 --- a/src/ui/StaticTaskCard.svelte +++ b/src/ui/StaticTaskCard.svelte @@ -1,5 +1,5 @@ {#if taskDisplayParams.mode === 'single-line'} @@ -127,24 +207,33 @@ on:click|stopPropagation={handleCheckboxClick} />
        - + -
        {task.content}
        - {#if descriptionProgress[1] * descriptionProgress[0] > 0 && !task.completed } - + {#if descriptionProgress[1] * descriptionProgress[0] > 0 && !task.completed} + {/if} - +
        {#if task.hasProject()}
        - +
        -
        +
        {:else} -
        @@ -211,14 +300,17 @@
        - {#if descriptionProgress[1] * descriptionProgress[0] > 0 } + {#if descriptionProgress[1] * descriptionProgress[0] > 0}
        - +
        {/if} {#if task.hasDescription()}
        - {@html descriptionMarkdown} + {@html taskDescriptionHTML}
        {/if}
        @@ -227,9 +319,24 @@
        - - - + + +
        {#each labelModule.getLabels() as label} @@ -252,7 +359,6 @@
        {/if} - \ No newline at end of file + From 03f3516196c4f4a7a98251d683e7220acf659cc6 Mon Sep 17 00:00:00 2001 From: terryli710 Date: Tue, 12 Mar 2024 09:57:58 -0700 Subject: [PATCH 20/20] fixed duration display bug --- src/ui/Duration.svelte | 9 +++++---- src/ui/StaticTaskCard.svelte | 26 ++++++++++++-------------- 2 files changed, 17 insertions(+), 18 deletions(-) diff --git a/src/ui/Duration.svelte b/src/ui/Duration.svelte index b9c5574..d1030a6 100644 --- a/src/ui/Duration.svelte +++ b/src/ui/Duration.svelte @@ -15,8 +15,10 @@ export let params: TaskDisplayParams; export let displayDuration: boolean; + const interactiveMode = interactive; + let duration: Duration; - if (interactive) { + if (interactiveMode) { duration = taskSyncManager.obsidianTask.hasDuration() ? taskSyncManager.obsidianTask.duration : undefined; } else { duration = taskItem.duration; @@ -105,7 +107,6 @@ new Notice(`[TaskCard] Invalid duration format: ${durationInputString}`); } } - taskSyncManager.updateObsidianTaskAttribute('duration', duration); origDurationInputString = durationInputString; @@ -138,7 +139,7 @@ function parseDurationInput(input: string): { hours: number, minutes: number } | } $: { - if (interactive) { + if (interactiveMode) { displayDuration = taskSyncManager.obsidianTask.hasDuration() || taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'; } else { displayDuration = !!taskItem.duration; // The double bang '!!' converts a truthy/falsy value to a boolean true/false @@ -162,7 +163,7 @@ function parseDurationInput(input: string): { hours: number, minutes: number } |
        - {#if taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'} + {#if interactiveMode && taskSyncManager.getTaskCardStatus('durationStatus') === 'editing'} elements, ensuring 'task-list-item' * class is present on relevant
      • elements, and adjusting elements inside these list items to have the * appropriate attributes. - * + * * @param {string} taskDescriptionHTML - The HTML string generated by the markdown parser for a task description. * @returns {string} The transformed HTML string with the updated structure. */ @@ -144,8 +144,7 @@ wrapperDiv.appendChild(ulElement); let lis = doc.body.querySelectorAll('li'); - console.log('doc', doc); - // console.log("lis", lis); + lis.forEach((li, index) => { let newLi = document.createElement('li'); let lineIndex = index + 1; @@ -154,7 +153,8 @@ newLi.setAttribute('data-line', lineIndex.toString()); newLi.setAttribute('data-real-line', lineIndex.toString()); - if (input instanceof HTMLInputElement) { // Ensure input is an HTMLInputElement + if (input instanceof HTMLInputElement) { + // Ensure input is an HTMLInputElement let clonedInput = input.cloneNode(true) as HTMLInputElement; // Cast the cloned node clonedInput.setAttribute('class', 'task-list-item-checkbox'); clonedInput.setAttribute('data-line', lineIndex.toString()); @@ -183,7 +183,6 @@ } ulElement.appendChild(newLi); - }); // Use XMLSerializer to convert the document back to a string @@ -192,7 +191,6 @@ return transformedHTML; } - {#if taskDisplayParams.mode === 'single-line'} @@ -321,7 +319,7 @@ @@ -329,13 +327,13 @@ interactive={false} params={{ mode: 'multi-line' }} taskItem={task} - {displayDuration} + displayDuration={displayDuration} />