diff --git a/.gitignore b/.gitignore
index b46578861a..7533d8a623 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,3 +34,12 @@ yarn-error.log
# Backup files.
*.bak
+
+# vscode files
+.vscode/*
+!.vscode/settings.json
+!.vscode/tasks.json
+!.vscode/launch.json
+!.vscode/extensions.json
+!.vscode/*.code-snippets
+main.css
diff --git a/.vscode/extensions.json b/.vscode/extensions.json
deleted file mode 100644
index 212e327868..0000000000
--- a/.vscode/extensions.json
+++ /dev/null
@@ -1,5 +0,0 @@
-{
- "recommendations": [
- "DavidAnson.vscode-markdownlint"
- ]
-}
diff --git a/.vscode/settings.json b/.vscode/settings.json
deleted file mode 100644
index 5623ddbe2e..0000000000
--- a/.vscode/settings.json
+++ /dev/null
@@ -1,3 +0,0 @@
-{
- "npm.packageManager": "yarn"
-}
diff --git a/docs/Advanced/Notifications.md b/docs/Advanced/Notifications.md
index 02f88d8daf..a1a4169f22 100644
--- a/docs/Advanced/Notifications.md
+++ b/docs/Advanced/Notifications.md
@@ -6,6 +6,9 @@ publish: true
#plugin/reminder
+> [!Tip]
+> Since Tasks X.Y.Z, the Tasks plugin has built-in support for reminders. See [[Task-Reminders]] and [[Dates#Reminder date and time]].
+
Within Tasks, notifications can be made possible by utilizing [obsidian-reminder](https://github.com/uphy/obsidian-reminder).
This utilizes the standard Tasks date (as the due date) and can be extended with an additional reminder date by including a ⏰ and a date/time in the format `⏰ YYYY-MM-DD HH:MM`.
Further, a default reminder can be enabled based on the Tasks' 'Due Date'.
diff --git a/docs/Getting Started/Dates.md b/docs/Getting Started/Dates.md
index c4d9f35070..928e81b0d8 100644
--- a/docs/Getting Started/Dates.md
+++ b/docs/Getting Started/Dates.md
@@ -81,6 +81,25 @@ starts before tomorrow
---
+### Reminder date and time
+
+You can ask Tasks for a reminder to do a task. And unlike all the other dates, reminders can have times.
+
+Reminder dates use an alarm clock emoji: ⏰
+
+```markdown
+- [ ] take out the trash ⏰ 2021-04-09 09:50
+```
+
+This differs from the [[Notifications|Reminders plugin]], which uses ⏰.
+
+For more information on the Reminders facility in Tasks, see [[Task-Reminders]].
+
+> [!released]
+> Reminder support was introduced in Tasks X.Y.Z.
+
+---
+
## Track task histories
This section explains the types of dates that Tasks can add for you automatically.
diff --git a/docs/Queries/Sorting.md b/docs/Queries/Sorting.md
index 0d91095c88..fa499c1fad 100644
--- a/docs/Queries/Sorting.md
+++ b/docs/Queries/Sorting.md
@@ -35,13 +35,15 @@ You can sort tasks by the following properties.
1. `created` (the date when the task was created)
1. `start` (the date when the task starts)
1. `scheduled` (the date when the task is scheduled)
+1. `reminder` (the reminder date and optional time)
1. `due` (the date when the task is due)
1. `done` (the date when the task was done)
1. `happens` (the earliest of start date, scheduled date, and due date)
> [!released]
`sort by happens` was introduced in Tasks 1.21.0.
-`sort by created` was introduced in Tasks 2.0.0.
+`sort by created` was introduced in Tasks 2.0.0.
+`sort by reminder` was introduced in Tasks X.Y.Z.
### Task statuses
diff --git a/docs/advanced/Task-Reminders.md b/docs/advanced/Task-Reminders.md
new file mode 100644
index 0000000000..4f0fd1f7d7
--- /dev/null
+++ b/docs/advanced/Task-Reminders.md
@@ -0,0 +1,31 @@
+---
+publish: true
+---
+
+# Tasks Reminders
+
+#plugin/reminder
+
+> [!released]
+> Reminder support was introduced in Tasks X.Y.Z.
+
+Within Tasks, reminder notifications can be set using either of two formats:
+
+- the standard Tasks format `⏰️ YYYY-MM-DD` for daily notifications at a set time
+- or by specifying the time as well, in 24-hour clock `⏰️ YYYY-MM-DD HH:mm`.
+
+## Limitations
+
+- It's not possible to set a reminder for midnight 12:00 am. This due to a limitation with how tasks creates dates using `momentjs` which sets the defaults time to midnight when one isn't provided.
+- System notifications don't work on Mobile because Obsidian doesn't provide an API.
+
+## How to complete the reminder
+
+The reminder date doesn't change when completing the task, the date will change only when you complete it from the reminder popup or from the notification.
+
+
+
+
+## Acknowledgment
+
+This feature was created using code from uphy's [obsidian-reminder](https://github.com/uphy/obsidian-reminder) plugin.
diff --git a/src/Commands/CreateOrEditTaskParser.ts b/src/Commands/CreateOrEditTaskParser.ts
index d97ed8a890..f45c31a2f1 100644
--- a/src/Commands/CreateOrEditTaskParser.ts
+++ b/src/Commands/CreateOrEditTaskParser.ts
@@ -60,6 +60,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
recurrence: null,
blockLink: '',
tags: [],
+ reminder: null,
originalMarkdown: '',
scheduledDateIsInferred: false,
});
@@ -95,6 +96,7 @@ export const taskFromLine = ({ line, path }: { line: string; path: string }): Ta
doneDate: null,
recurrence: null,
tags: [],
+ reminder: null,
originalMarkdown: '',
// Not needed since the inferred status is always re-computed after submitting.
scheduledDateIsInferred: false,
diff --git a/src/Config/Settings.ts b/src/Config/Settings.ts
index bc71f2b265..696766f008 100644
--- a/src/Config/Settings.ts
+++ b/src/Config/Settings.ts
@@ -6,6 +6,7 @@ import { Status } from '../Status';
import { DefaultTaskSerializer, type TaskSerializer } from '../TaskSerializer';
import type { SuggestionBuilder } from '../Suggestor';
import { DataviewTaskSerializer } from '../TaskSerializer/DataviewTaskSerializer';
+import { ReminderSettings } from '../Reminders/Reminder';
import { DebugSettings } from './DebugSettings';
import { StatusSettings } from './StatusSettings';
import { Feature } from './Feature';
@@ -49,6 +50,11 @@ export const TASK_FORMATS = {
export type TASK_FORMATS = typeof TASK_FORMATS; // For convenience to make some typing easier
+// Time formats for the reminder settings.
+export const TIME_FORMATS = {
+ twentyFourHour: 'YYYY-MM-DD HH:mm',
+};
+
export interface Settings {
globalQuery: string;
globalFilter: string;
@@ -74,6 +80,8 @@ export interface Settings {
// dynamically generated.
generalSettings: SettingsMap;
+ reminderSettings: ReminderSettings;
+
// Tracks the stage of the headings in the settings UI.
headingOpened: HeadingState;
debugSettings: DebugSettings;
@@ -106,6 +114,7 @@ const defaultSettings: Settings = {
},
headingOpened: {},
debugSettings: new DebugSettings(),
+ reminderSettings: new ReminderSettings(),
};
let settings: Settings = { ...defaultSettings };
diff --git a/src/Config/SettingsTab.ts b/src/Config/SettingsTab.ts
index eab3f68c68..d3edb5decf 100644
--- a/src/Config/SettingsTab.ts
+++ b/src/Config/SettingsTab.ts
@@ -223,6 +223,46 @@ export class SettingsTab extends PluginSettingTab {
});
});
+ // ---------------------------------------------------------------------------
+ containerEl.createEl('h4', { text: 'Reminder Settings' });
+ // ---------------------------------------------------------------------------
+
+ new Setting(containerEl)
+ .setName('Daily Reminder Time')
+ .setDesc(
+ SettingsTab.createFragmentWithHTML(
+ '
When daily reminders should be triggered. Should be in the 24-hour clock, for example 09:00 or 21:00.
',
+ ),
+ )
+ .addText((text) => {
+ const settings = getSettings().reminderSettings;
+ text.setPlaceholder('10')
+ .setValue(settings.dailyReminderTime)
+ .onChange(async (value) => {
+ settings.dailyReminderTime = value;
+ updateSettings({ reminderSettings: settings });
+ await this.plugin.saveSettings();
+ });
+ });
+
+ new Setting(containerEl)
+ .setName('Check Reminders interval')
+ .setDesc(
+ SettingsTab.createFragmentWithHTML('How often Tasks should check for reminders in Seconds.
'),
+ )
+ .addSlider((slider) => {
+ const settings = getSettings().reminderSettings;
+ slider
+ .setLimits(1, 30, 1)
+ .setValue(settings.refreshIntervalMilliseconds / 1000) // convert from miliseconds to seconds
+ .setDynamicTooltip()
+ .onChange(async (value) => {
+ settings.refreshIntervalMilliseconds = value * 1000; // convert from seconds to miliseconds
+ updateSettings({ reminderSettings: settings });
+ await this.plugin.saveSettings();
+ });
+ });
+
// ---------------------------------------------------------------------------
containerEl.createEl('h4', { text: 'Recurring task Settings' });
// ---------------------------------------------------------------------------
diff --git a/src/Query/Filter/ReminderDateField.ts b/src/Query/Filter/ReminderDateField.ts
new file mode 100644
index 0000000000..26b7afa3c6
--- /dev/null
+++ b/src/Query/Filter/ReminderDateField.ts
@@ -0,0 +1,69 @@
+import type { Moment } from 'moment';
+import type { Task } from '../../Task';
+import type { Comparator } from '../Sorter';
+import { compareByDate } from '../../lib/DateTools';
+import { DateField } from './DateField';
+
+/**
+ * ReminderDateField provides filter, sorting and grouping of tasks by their reminder value.
+ *
+ * **Filtering** by reminder ignores times completely. Only the dates in the task reminders
+ * and in the filter line are used. It is not possible to search for reminders at a particular
+ * time of day.
+ *
+ * **Sorting** by reminder does use the reminder times, if supplied.
+ */
+export class ReminderDateField extends DateField {
+ public fieldName(): string {
+ return 'reminder';
+ }
+
+ /**
+ * Return the reminder date **with any time stripped off**.
+ *
+ * In order for filtering by reminder to work correctly, this returns the reminder date
+ * with any time stripped off, because the date-filtering works in whole days currently,
+ * due to all the other date fields not supporting time.
+ *
+ * If you want the reminder date and time, use {@link dateWithTime}.
+ * @param task
+ * @see dateWithTime
+ */
+ public date(task: Task): Moment | null {
+ if (task.reminder) {
+ return task.reminder.time.startOf('day');
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Return the reminder date including its time.
+ *
+ * If you want just the date, with time stripped off, use {@link date}.
+ * @param task
+ * @see date
+ */
+ public dateWithTime(task: Task): Moment | null {
+ if (task.reminder) {
+ return task.reminder.time;
+ } else {
+ return null;
+ }
+ }
+
+ protected filterResultIfFieldMissing() {
+ return false;
+ }
+
+ /**
+ * Return a function to compare two Task objects by their reminder.
+ *
+ * @note This does use any time on reminders, for more precise sorting.
+ */
+ public comparator(): Comparator {
+ return (a: Task, b: Task) => {
+ return compareByDate(this.dateWithTime(a), this.dateWithTime(b));
+ };
+ }
+}
diff --git a/src/Query/FilterParser.ts b/src/Query/FilterParser.ts
index c53662445e..9548a054fc 100644
--- a/src/Query/FilterParser.ts
+++ b/src/Query/FilterParser.ts
@@ -3,6 +3,7 @@ import { DescriptionField } from './Filter/DescriptionField';
import { CreatedDateField } from './Filter/CreatedDateField';
import { DoneDateField } from './Filter/DoneDateField';
import { DueDateField } from './Filter/DueDateField';
+import { ReminderDateField } from './Filter/ReminderDateField';
import { ExcludeSubItemsField } from './Filter/ExcludeSubItemsField';
import { HeadingField } from './Filter/HeadingField';
import { PathField } from './Filter/PathField';
@@ -44,6 +45,7 @@ const fieldCreators: EndsWith = [
() => new ScheduledDateField(),
() => new DueDateField(),
() => new DoneDateField(),
+ () => new ReminderDateField(),
() => new PathField(),
() => new FolderField(),
() => new RootField(),
diff --git a/src/Query/Query.ts b/src/Query/Query.ts
index ca644c8c8c..65f07c6537 100644
--- a/src/Query/Query.ts
+++ b/src/Query/Query.ts
@@ -21,7 +21,7 @@ export class Query implements IQuery {
private _grouping: Grouper[] = [];
private readonly hideOptionsRegexp =
- /^(hide|show) (task count|backlink|priority|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|urgency)/;
+ /^(hide|show) (task count|backlink|priority|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|urgency|reminder date)/;
private readonly shortModeRegexp = /^short/;
private readonly explainQueryRegexp = /^explain/;
@@ -216,6 +216,9 @@ export class Query implements IQuery {
case 'due date':
this._layoutOptions.hideDueDate = hide;
break;
+ case 'reminder date':
+ this._layoutOptions.hideReminderDate = hide;
+ break;
case 'done date':
this._layoutOptions.hideDoneDate = hide;
break;
diff --git a/src/Recurrence.ts b/src/Recurrence.ts
index 18bbb2c948..81e067bd09 100644
--- a/src/Recurrence.ts
+++ b/src/Recurrence.ts
@@ -1,6 +1,7 @@
import type { Moment } from 'moment';
import { RRule } from 'rrule';
import { compareByDate } from './lib/DateTools';
+import { Reminder, isReminderSame } from './Reminders/Reminder';
export class Recurrence {
private readonly rrule: RRule;
@@ -8,6 +9,7 @@ export class Recurrence {
private readonly startDate: Moment | null;
private readonly scheduledDate: Moment | null;
private readonly dueDate: Moment | null;
+ private readonly reminder: Reminder | null;
/**
* The reference date is used to calculate future occurrences.
@@ -31,6 +33,7 @@ export class Recurrence {
startDate,
scheduledDate,
dueDate,
+ reminder,
}: {
rrule: RRule;
baseOnToday: boolean;
@@ -38,6 +41,7 @@ export class Recurrence {
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
+ reminder: Reminder | null;
}) {
this.rrule = rrule;
this.baseOnToday = baseOnToday;
@@ -45,6 +49,7 @@ export class Recurrence {
this.startDate = startDate;
this.scheduledDate = scheduledDate;
this.dueDate = dueDate;
+ this.reminder = reminder;
}
public static fromText({
@@ -52,11 +57,13 @@ export class Recurrence {
startDate,
scheduledDate,
dueDate,
+ reminder,
}: {
recurrenceRuleText: string;
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
+ reminder: Reminder | null;
}): Recurrence | null {
try {
const match = recurrenceRuleText.match(/^([a-zA-Z0-9, !]+?)( when done)?$/i);
@@ -79,6 +86,8 @@ export class Recurrence {
referenceDate = window.moment(scheduledDate);
} else if (startDate) {
referenceDate = window.moment(startDate);
+ } else if (reminder) {
+ referenceDate = window.moment(reminder.time.format('YYYY-MM-DD'));
}
if (!baseOnToday && referenceDate !== null) {
@@ -95,6 +104,7 @@ export class Recurrence {
startDate,
scheduledDate,
dueDate,
+ reminder: reminder,
});
}
} catch (error) {
@@ -120,6 +130,7 @@ export class Recurrence {
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
+ reminder: Reminder | null;
} | null {
const next = this.nextReferenceDate();
@@ -129,6 +140,7 @@ export class Recurrence {
let startDate: Moment | null = null;
let scheduledDate: Moment | null = null;
let dueDate: Moment | null = null;
+ let reminders: Reminder | null = null;
// Only if a reference date is given. A reference date will exist if at
// least one of the other dates is set.
@@ -157,12 +169,26 @@ export class Recurrence {
// Rounding days to handle cross daylight-savings-time recurrences.
dueDate.add(Math.round(originalDifference.asDays()), 'days');
}
+ if (this.reminder) {
+ {
+ // TODO Remove braces - added to minimise diffs in large change
+ const reminder = this.reminder; // TODO Inline reminder
+ const originalDifference = window.moment.duration(reminder.time.diff(this.referenceDate));
+ const remTime = window.moment(next);
+
+ remTime.add(Math.floor(originalDifference.asDays()), 'days');
+ // add back time
+ remTime.set({ hour: reminder.time.hour(), minute: reminder.time.minute() });
+ reminders = new Reminder(remTime, reminder.type);
+ }
+ }
}
return {
startDate,
scheduledDate,
dueDate,
+ reminder: reminders,
};
}
@@ -185,6 +211,10 @@ export class Recurrence {
return false;
}
+ if (!isReminderSame(this.reminder, other.reminder)) {
+ return false;
+ }
+
return this.toText() === other.toText(); // this also checks baseOnToday
}
diff --git a/src/Reminders/Components/Icon.svelte b/src/Reminders/Components/Icon.svelte
new file mode 100644
index 0000000000..60b8589efd
--- /dev/null
+++ b/src/Reminders/Components/Icon.svelte
@@ -0,0 +1,18 @@
+
+
+
+
+
diff --git a/src/Reminders/Components/Markdown.svelte b/src/Reminders/Components/Markdown.svelte
new file mode 100644
index 0000000000..0d3a8b1712
--- /dev/null
+++ b/src/Reminders/Components/Markdown.svelte
@@ -0,0 +1,20 @@
+
+
+
diff --git a/src/Reminders/Components/Reminder.svelte b/src/Reminders/Components/Reminder.svelte
new file mode 100644
index 0000000000..d8db028846
--- /dev/null
+++ b/src/Reminders/Components/Reminder.svelte
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+
+
+ {task.filename ?? "filename was null"}
+
+
+
+ Mark as Done
+
+
+
+ Remind Me Later
+
+ {#each laters as i}
+ {"later.label"}
+ {/each}
+
+
+
+
+
diff --git a/src/Reminders/Notification.ts b/src/Reminders/Notification.ts
new file mode 100644
index 0000000000..0b6e605ace
--- /dev/null
+++ b/src/Reminders/Notification.ts
@@ -0,0 +1,190 @@
+import { App, Modal } from 'obsidian';
+import type { Moment } from 'moment';
+import { TIME_FORMATS, getSettings } from '../Config/Settings';
+import type { Task } from '../Task';
+import { Query } from '../Query/Query';
+import { sameDateTime } from '../lib/DateTools';
+import type { Cache } from '../Cache';
+import { getTaskLineAndFile } from '../File';
+import ReminderView from './Components/Reminder.svelte';
+import { Reminder, ReminderType } from './Reminder';
+
+export class TaskNotification {
+ // this is the default reminder settings not getting updated
+ constructor(private app: App) {}
+
+ public show(task: Task) {
+ const { reminderSettings } = getSettings();
+ const electron = require('electron');
+ const Notification = electron.remote.Notification;
+
+ // if election notification is supported, aka desktop app
+ if (Notification.isSupported()) {
+ // Show system notification
+ const n = new Notification({
+ title: reminderSettings.notificationTitle,
+ body: task.description,
+ });
+ n.on('click', () => {
+ console.log('Notification clicked');
+ n.close();
+ this.showBuiltinReminder(task);
+ });
+ // Notification actions only supported in macOS
+ {
+ n.on('action', (_: any, index: any) => {
+ if (index === 0) {
+ this.onDone(task);
+ return;
+ }
+ });
+ const actions = [{ type: 'button', text: 'Mark as Done' }];
+ n.actions = actions as any;
+ }
+
+ n.show();
+ } else {
+ // Show obsidian modal notification for mobile users, must be in app
+ this.showBuiltinReminder(task);
+ }
+ }
+
+ public watcher(cache: Cache): number | undefined {
+ let intervalTaskRunning = false;
+ const { reminderSettings } = getSettings();
+ // Set up the recurring check for reminders.
+ return window.setInterval(() => {
+ if (intervalTaskRunning) {
+ // console.log('Skip reminder interval task because task is already running.');
+ return;
+ }
+ intervalTaskRunning = true;
+
+ const tasks = cache.getTasks();
+ this.reminderEvent(tasks).finally(() => {
+ intervalTaskRunning = false;
+ });
+ }, reminderSettings.refreshIntervalMilliseconds);
+ }
+
+ private async reminderEvent(tasks: Task[]): Promise {
+ if (!tasks?.length) {
+ return; // No tasks, nothing to do.
+ }
+
+ // get list of all future reminders that are not done
+ const input = 'reminder after yesterday\nnot done';
+ const query = new Query({ source: input });
+
+ // Get list of tasks with reminders
+ let reminderTasks = [...tasks];
+ query.filters.forEach((filter) => {
+ reminderTasks = reminderTasks.filter(filter.filterFunction);
+ });
+
+ const { reminderSettings } = getSettings();
+ for (const task of reminderTasks) {
+ const dailyReminderTime = window.moment(
+ `${window.moment().format('YYYY-MM-DD')} ${reminderSettings.dailyReminderTime}`,
+ TIME_FORMATS.twentyFourHour,
+ );
+
+ if (task.reminder) {
+ const rDate = task.reminder; // TODO Inline rDate
+ const curTime = window.moment();
+ if (TaskNotification.shouldNotifiy(rDate, dailyReminderTime, curTime)) {
+ rDate.notified = true;
+ this.show(task);
+ }
+ }
+ }
+ }
+
+ public static shouldNotifiy(rDate: Reminder, dailyReminderTime: Moment, curTime: Moment) {
+ if (!rDate.notified) {
+ // daily reminder
+ if (rDate.type === ReminderType.Date && sameDateTime(dailyReminderTime, curTime)) {
+ return true;
+ } else if (sameDateTime(rDate.time, curTime)) {
+ // specific time reminder
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private showBuiltinReminder(
+ reminder: any,
+ //onRemindMeLater: (time: DateTime) => void,
+ ) {
+ new ObsidianNotificationModal(
+ this.app,
+ [1, 2, 3, 4, 5],
+ reminder,
+ //onRemindMeLater,
+ this.onDone,
+ this.onOpenFile,
+ ).open();
+ }
+
+ // TODO How does this work with recurring tasks?
+ private onDone(task: Task) {
+ if (!task.status.isCompleted()) {
+ task.toggleUpdate();
+ }
+ }
+
+ // TODO Understand and test this.
+ // TODO What happens if this is invoked in a way that fails to find the task???
+ private async onOpenFile(task: Task) {
+ const result = await getTaskLineAndFile(task, app.vault);
+ if (result) {
+ const [line, file] = result;
+ const leaf = this.app.workspace.getLeaf('tab');
+ await leaf.openFile(file, { eState: { line: line } });
+ }
+ }
+}
+
+class ObsidianNotificationModal extends Modal {
+ constructor(
+ app: App,
+ private laters: Array,
+ private task: Task,
+ //private onRemindMeLater: (time: any) => void,
+ private onDone: (task: Task) => void,
+ private onOpenFile: (task: Task) => void,
+ ) {
+ super(app);
+ }
+
+ override onOpen() {
+ const { contentEl } = this;
+ new ReminderView({
+ target: contentEl,
+ props: {
+ task: this.task,
+ laters: this.laters,
+ component: this,
+ onRemindMeLater: () => {
+ // this.onRemindMeLater(time);
+ this.close();
+ },
+ onDone: () => {
+ this.onDone(this.task);
+ this.close();
+ },
+ onOpenFile: () => {
+ this.onOpenFile(this.task);
+ this.close();
+ },
+ },
+ });
+ }
+
+ override onClose() {
+ // Unset the reminder from being displayed. This lets other parts of the plugin continue.
+ const { contentEl } = this;
+ contentEl.empty();
+ }
+}
diff --git a/src/Reminders/Reminder.ts b/src/Reminders/Reminder.ts
new file mode 100644
index 0000000000..ac35fde2a9
--- /dev/null
+++ b/src/Reminders/Reminder.ts
@@ -0,0 +1,75 @@
+import type { Moment } from 'moment';
+import { TIME_FORMATS, getSettings } from '../Config/Settings';
+
+export class ReminderSettings {
+ notificationTitle: string = 'Task Reminder';
+ dateFormat: string = 'YYYY-MM-DD'; // TODO Do not put format strings in user settings
+ dailyReminderTime: string = '09:00';
+ refreshIntervalMilliseconds: number = 5 * 1000;
+
+ constructor() {}
+}
+
+export enum ReminderType {
+ Date,
+ DateTime,
+}
+
+export class Reminder {
+ public time: Moment;
+ public type: ReminderType;
+ public notified: boolean = false;
+
+ constructor(time: Moment, type: ReminderType) {
+ this.time = time;
+ this.type = type;
+ }
+
+ public toString(): string {
+ const reminderSettings = getSettings().reminderSettings;
+ if (this.type === ReminderType.Date) {
+ return this.time.format(reminderSettings.dateFormat);
+ }
+ return this.time.format(TIME_FORMATS.twentyFourHour);
+ }
+
+ // TODO Rename this to identicalTo() - for consistency with similar methods...?? Check how Task does it
+ isSame(other: Reminder | null) {
+ return isReminderSame(this, other);
+ }
+}
+
+// TODO Move this to a named constructor
+export function parseDateTime(dateTime: string): Reminder {
+ const reminder = window.moment(dateTime, TIME_FORMATS.twentyFourHour);
+ return parseMoment(reminder);
+}
+
+// TODO Move this to a named constructor
+export function parseMoment(reminder: Moment): Reminder {
+ if (reminder.format('h:mm a') === '12:00 am') {
+ //aka .startOf(day) which is the default time for reminders
+ return new Reminder(reminder, ReminderType.Date);
+ } else {
+ return new Reminder(reminder, ReminderType.DateTime);
+ }
+}
+
+// TODO Add detailed tests
+// TODO Move to Reminder class
+export function isReminderSame(a: Reminder | null, b: Reminder | null) {
+ if (a === null && b !== null) {
+ return false;
+ } else if (a !== null && b === null) {
+ return false;
+ } else if (a !== null && b !== null) {
+ if (!a.time.isSame(b.time)) {
+ return false;
+ }
+ if (a.type != b.type) {
+ return false;
+ }
+ }
+
+ return true;
+}
diff --git a/src/Suggestor/Suggestor.ts b/src/Suggestor/Suggestor.ts
index b30d2e1d74..ad61b285c7 100644
--- a/src/Suggestor/Suggestor.ts
+++ b/src/Suggestor/Suggestor.ts
@@ -273,6 +273,7 @@ function addRecurrenceSuggestions(line: string, cursorPos: number, settings: Set
startDate: null,
scheduledDate: null,
dueDate: null,
+ reminder: null,
})?.toText();
if (parsedRecurrence) {
const appendedText = `${recurrencePrefix} ${parsedRecurrence} `;
diff --git a/src/Task.ts b/src/Task.ts
index 741fb5d5ff..6f5b5b4abc 100644
--- a/src/Task.ts
+++ b/src/Task.ts
@@ -10,6 +10,9 @@ import { renderTaskLine } from './TaskLineRenderer';
import type { TaskLineRenderDetails } from './TaskLineRenderer';
import { DateFallback } from './DateFallback';
import { compareByDate } from './lib/DateTools';
+import type { Reminder } from './Reminders/Reminder';
+import { replaceTaskWithTasks } from './File';
+import { isReminderSame } from './Reminders/Reminder';
/**
* When sorting, make sure low always comes after none. This way any tasks with low will be below any exiting
@@ -101,6 +104,7 @@ export class Task {
public readonly taskLocation: TaskLocation;
public readonly tags: string[];
+ public readonly reminder: Reminder | null; // TODO Move this to after doneDate??
public readonly priority: Priority;
@@ -139,6 +143,7 @@ export class Task {
recurrence,
blockLink,
tags,
+ reminder,
originalMarkdown,
scheduledDateIsInferred,
}: {
@@ -156,6 +161,7 @@ export class Task {
recurrence: Recurrence | null;
blockLink: string;
tags: string[] | [];
+ reminder: Reminder | null;
originalMarkdown: string;
scheduledDateIsInferred: boolean;
}) {
@@ -166,6 +172,7 @@ export class Task {
this.taskLocation = taskLocation;
this.tags = tags;
+ this.reminder = reminder;
this.priority = priority;
@@ -315,6 +322,7 @@ export class Task {
startDate: Moment | null;
scheduledDate: Moment | null;
dueDate: Moment | null;
+ reminder: Reminder | null;
} | null = null;
if (newStatus.isCompleted()) {
@@ -388,6 +396,18 @@ export class Task {
return recurrenceOnNextLine ? newTasks.reverse() : newTasks;
}
+ // toggle task status and update
+ // TODO Understand why this method exists.
+ // TODO Check that this method has tests.
+ public toggleUpdate() {
+ const newTasks = this.toggle();
+
+ replaceTaskWithTasks({
+ originalTask: this,
+ newTasks: newTasks,
+ });
+ }
+
public get urgency(): number {
if (this._urgency === null) {
this._urgency = Urgency.calculate(this);
@@ -526,6 +546,11 @@ export class Task {
return false;
}
+ // compare reminders
+ if (!isReminderSame(this.reminder, other.reminder)) {
+ return false;
+ }
+
// Compare Date fields
args = ['createdDate', 'startDate', 'scheduledDate', 'dueDate', 'doneDate'];
for (const el of args) {
diff --git a/src/TaskLayout.ts b/src/TaskLayout.ts
index 2597e7ec7f..8640dac634 100644
--- a/src/TaskLayout.ts
+++ b/src/TaskLayout.ts
@@ -11,6 +11,7 @@ export class LayoutOptions {
hideScheduledDate: boolean = false;
hideDoneDate: boolean = false;
hideDueDate: boolean = false;
+ hideReminderDate: boolean = false;
hideRecurrenceRule: boolean = false;
hideEditButton: boolean = false;
hideUrgency: boolean = true;
@@ -26,6 +27,7 @@ export type TaskLayoutComponent =
| 'startDate'
| 'scheduledDate'
| 'dueDate'
+ | 'reminders'
| 'doneDate'
| 'blockLink';
@@ -43,6 +45,7 @@ export class TaskLayout {
'startDate',
'scheduledDate',
'dueDate',
+ 'reminders',
'doneDate',
'blockLink',
];
@@ -100,6 +103,7 @@ export class TaskLayout {
newComponents = removeIf(newComponents, layoutOptions.hideScheduledDate, 'scheduledDate');
newComponents = removeIf(newComponents, layoutOptions.hideDueDate, 'dueDate');
newComponents = removeIf(newComponents, layoutOptions.hideDoneDate, 'doneDate');
+ newComponents = removeIf(newComponents, layoutOptions.hideReminderDate, 'reminders');
// The following components are handled in QueryRenderer.ts and thus are not part of the same flow that
// hides TaskLayoutComponent items. However, we still want to have 'tasks-layout-hide' items for them
// (see https://github.com/obsidian-tasks-group/obsidian-tasks/issues/1866).
diff --git a/src/TaskLineRenderer.ts b/src/TaskLineRenderer.ts
index dd94f78d13..f604549beb 100644
--- a/src/TaskLineRenderer.ts
+++ b/src/TaskLineRenderer.ts
@@ -25,6 +25,7 @@ export const LayoutClasses: { [c in TaskLayoutComponent]: string } = {
createdDate: 'task-created',
scheduledDate: 'task-scheduled',
doneDate: 'task-done',
+ reminders: 'task-reminders',
recurrenceRule: 'task-recurring',
blockLink: '',
};
diff --git a/src/TaskSerializer/DataviewTaskSerializer.ts b/src/TaskSerializer/DataviewTaskSerializer.ts
index b7f2519d3a..22007bd6b1 100644
--- a/src/TaskSerializer/DataviewTaskSerializer.ts
+++ b/src/TaskSerializer/DataviewTaskSerializer.ts
@@ -71,6 +71,7 @@ export const DATAVIEW_SYMBOLS = {
dueDateSymbol: 'due::',
doneDateSymbol: 'completion::',
recurrenceSymbol: 'repeat::',
+ reminderDateSymbol: 'remind::',
TaskFormatRegularExpressions: {
priorityRegex: toInlineFieldRegex(/priority:: *(high|medium|low)/),
startDateRegex: toInlineFieldRegex(/start:: *(\d{4}-\d{2}-\d{2})/),
@@ -79,6 +80,11 @@ export const DATAVIEW_SYMBOLS = {
dueDateRegex: toInlineFieldRegex(/due:: *(\d{4}-\d{2}-\d{2})/),
doneDateRegex: toInlineFieldRegex(/completion:: *(\d{4}-\d{2}-\d{2})/),
recurrenceRegex: toInlineFieldRegex(/repeat:: *([a-zA-Z0-9, !]+)/),
+ // TODO Remove the duplication of regex from emoji parsing code
+ // TODO Add tests and then actually hook up reminder reading in dataview format
+ reminderRegex: toInlineFieldRegex(
+ /remind:: *((\d{4}-\d{2}-\d{2}(?: \d{1,2}:\d{2} (?:am|pm|PM|AM))?\s*(?:,\s*)?)+)\b/,
+ ),
},
} as const;
diff --git a/src/TaskSerializer/DefaultTaskSerializer.ts b/src/TaskSerializer/DefaultTaskSerializer.ts
index 73ce50fc0a..56b631d814 100644
--- a/src/TaskSerializer/DefaultTaskSerializer.ts
+++ b/src/TaskSerializer/DefaultTaskSerializer.ts
@@ -1,4 +1,6 @@
import type { Moment } from 'moment';
+import { TIME_FORMATS } from '../Config/Settings';
+import { Reminder, parseMoment } from '../Reminders/Reminder';
import { TaskLayout } from '../TaskLayout';
import type { TaskLayoutComponent } from '../TaskLayout';
import { Recurrence } from '../Recurrence';
@@ -24,6 +26,7 @@ export interface DefaultTaskSerializerSymbols {
readonly dueDateSymbol: string;
readonly doneDateSymbol: string;
readonly recurrenceSymbol: string;
+ readonly reminderDateSymbol: string;
readonly TaskFormatRegularExpressions: {
priorityRegex: RegExp;
startDateRegex: RegExp;
@@ -32,6 +35,7 @@ export interface DefaultTaskSerializerSymbols {
dueDateRegex: RegExp;
doneDateRegex: RegExp;
recurrenceRegex: RegExp;
+ reminderRegex: RegExp;
};
}
@@ -52,6 +56,7 @@ export const DEFAULT_SYMBOLS: DefaultTaskSerializerSymbols = {
dueDateSymbol: '📅',
doneDateSymbol: '✅',
recurrenceSymbol: '🔁',
+ reminderDateSymbol: '⏰️',
TaskFormatRegularExpressions: {
// The following regex's end with `$` because they will be matched and
// removed from the end until none are left.
@@ -62,6 +67,7 @@ export const DEFAULT_SYMBOLS: DefaultTaskSerializerSymbols = {
dueDateRegex: /[📅📆🗓] *(\d{4}-\d{2}-\d{2})$/u,
doneDateRegex: /✅ *(\d{4}-\d{2}-\d{2})$/u,
recurrenceRegex: /🔁 ?([a-zA-Z0-9, !]+)$/iu,
+ reminderRegex: /⏰️ *(\d{4}-\d{2}-\d{2}(?: \d{2}:\d{2})?)/u,
},
} as const;
@@ -95,6 +101,7 @@ export class DefaultTaskSerializer implements TaskSerializer {
doneDateSymbol,
recurrenceSymbol,
dueDateSymbol,
+ reminderDateSymbol,
} = this.symbols;
switch (component) {
@@ -137,6 +144,11 @@ export class DefaultTaskSerializer implements TaskSerializer {
return layout.options.shortMode
? ' ' + dueDateSymbol
: ` ${dueDateSymbol} ${task.dueDate.format(TaskRegularExpressions.dateFormat)}`;
+ case 'reminders': // TODO Rename to singular
+ if (!task.reminder) return '';
+ return layout.options.shortMode
+ ? ' ' + reminderDateSymbol
+ : ` ${reminderDateSymbol} ${task.reminder.toString()}`;
case 'recurrenceRule':
if (!task.recurrence) return '';
return layout.options.shortMode
@@ -189,6 +201,7 @@ export class DefaultTaskSerializer implements TaskSerializer {
let scheduledDate: Moment | null = null;
let dueDate: Moment | null = null;
let doneDate: Moment | null = null;
+ let rList: Reminder | null = null; // TODO Rename to reminder
let createdDate: Moment | null = null;
let recurrenceRule: string = '';
let recurrence: Recurrence | null = null;
@@ -265,6 +278,14 @@ export class DefaultTaskSerializer implements TaskSerializer {
trailingTags = trailingTags.length > 0 ? [tagName, trailingTags].join(' ') : tagName;
}
+ const reminderMatch = line.match(TaskFormatRegularExpressions.reminderRegex);
+ if (reminderMatch !== null) {
+ line = line.replace(TaskFormatRegularExpressions.reminderRegex, '').trim();
+ const reminderDate2 = reminderMatch[1];
+ const reminder = window.moment(reminderDate2, TIME_FORMATS.twentyFourHour);
+ rList = parseMoment(reminder);
+ matched = true;
+ }
runs++;
} while (matched && runs <= maxRuns);
@@ -275,6 +296,7 @@ export class DefaultTaskSerializer implements TaskSerializer {
startDate,
scheduledDate,
dueDate,
+ reminder: rList,
});
}
// Add back any trailing tags to the description. We removed them so we can parse the rest of the
@@ -293,6 +315,7 @@ export class DefaultTaskSerializer implements TaskSerializer {
doneDate,
recurrence,
tags: Task.extractHashtags(line),
+ reminder: rList,
};
}
}
diff --git a/src/TaskSerializer/index.ts b/src/TaskSerializer/index.ts
index 85d6820122..301f67c2d6 100644
--- a/src/TaskSerializer/index.ts
+++ b/src/TaskSerializer/index.ts
@@ -20,6 +20,7 @@ export type TaskDetails = Writeable<
| 'doneDate'
| 'recurrence'
| 'tags'
+ | 'reminder'
>
>;
diff --git a/src/lib/DateTools.ts b/src/lib/DateTools.ts
index f6581527f9..ffbed6241a 100644
--- a/src/lib/DateTools.ts
+++ b/src/lib/DateTools.ts
@@ -21,3 +21,7 @@ export function compareByDate(a: moment.Moment | null, b: moment.Moment | null):
return 0;
}
}
+
+export function sameDateTime(a: moment.Moment, b: moment.Moment) {
+ return a.format('YYYY-MM-DD HH:mm') === b.format('YYYY-MM-DD HH:mm');
+}
diff --git a/src/main.ts b/src/main.ts
index c55f403e86..1217611c40 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -1,5 +1,6 @@
import { Plugin } from 'obsidian';
+import { TaskNotification } from './Reminders/Notification';
import { Cache } from './Cache';
import { Commands } from './Commands';
import { TasksEvents } from './TasksEvents';
@@ -20,6 +21,7 @@ export default class TasksPlugin extends Plugin {
private cache: Cache | undefined;
public inlineRenderer: InlineRenderer | undefined;
public queryRenderer: QueryRenderer | undefined;
+ private taskNotification: TaskNotification | undefined;
get apiV1() {
return tasksApiV1(app);
@@ -28,6 +30,7 @@ export default class TasksPlugin extends Plugin {
async onload() {
logging.registerConsoleLogger();
console.log('loading plugin "tasks"');
+ this.taskNotification = new TaskNotification(this.app);
await this.loadSettings();
this.addSettingTab(new SettingsTab({ plugin: this }));
@@ -53,6 +56,14 @@ export default class TasksPlugin extends Plugin {
this.registerEditorExtension(newLivePreviewExtension());
this.registerEditorSuggest(new EditorSuggestor(this.app, getSettings()));
new Commands({ plugin: this });
+
+ // Register the watcher for reminders.
+ this.app.workspace.onLayoutReady(async () => {
+ if (this.taskNotification && this.cache) {
+ this.registerInterval(this.taskNotification.watcher(this.cache) ?? 0);
+ }
+ // TODO Is it possible for this.cache to not yet be set up?
+ });
}
async loadTaskStatuses() {
diff --git a/src/ui/EditTask.svelte b/src/ui/EditTask.svelte
index ea48fbc471..6506867fcd 100644
--- a/src/ui/EditTask.svelte
+++ b/src/ui/EditTask.svelte
@@ -2,11 +2,12 @@
import * as chrono from 'chrono-node';
import { onMount } from 'svelte';
import { Recurrence } from '../Recurrence';
- import { getSettings, TASK_FORMATS } from '../Config/Settings';
+ import { getSettings, TASK_FORMATS, TIME_FORMATS} from '../Config/Settings';
import { GlobalFilter } from '../Config/GlobalFilter';
import { Status } from '../Status';
import { Priority, Task } from '../Task';
import { doAutocomplete } from '../DateAbbreviations';
+ import { parseMoment } from '../Reminders/Reminder';
// These exported variables are passed in as props by TaskModal.onOpen():
export let task: Task;
@@ -19,6 +20,7 @@
startDateSymbol,
scheduledDateSymbol,
dueDateSymbol,
+ reminderDateSymbol,
} = TASK_FORMATS.tasksPluginEmoji.taskSerializer.symbols;
let descriptionInput: HTMLTextAreaElement;
@@ -32,6 +34,7 @@
scheduledDate: string;
dueDate: string;
doneDate: string;
+ reminderDate: string;
forwardOnly: boolean;
} = {
description: '',
@@ -43,6 +46,7 @@
scheduledDate: '',
dueDate: '',
doneDate: '',
+ reminderDate: '',
forwardOnly: true
};
@@ -54,6 +58,8 @@
let isScheduledDateValid: boolean = true;
let parsedDueDate: string = '';
let isDueDateValid: boolean = true;
+ let parsedReminderDate: string = '';
+ let isReminderDateValid: boolean = true;
let parsedRecurrence: string = '';
let isRecurrenceValid: boolean = true;
let parsedDone: string = '';
@@ -110,7 +116,7 @@
* @returns the parsed date string. Includes "invalid" if {@code typedDate} was invalid.
*/
function parseTypedDateForDisplay(
- fieldName: 'created' | 'start' | 'scheduled' | 'due' | 'done',
+ fieldName: 'created' | 'start' | 'scheduled' | 'due' | 'reminder' | 'done',
typedDate: string,
forwardDate: Date | undefined = undefined,
): string {
@@ -121,7 +127,11 @@
forwardDate: forwardDate != undefined,
});
if (parsed !== null) {
- return window.moment(parsed).format('YYYY-MM-DD');
+ if (fieldName === 'reminder') {
+ return window.moment(parsed).format(TIME_FORMATS.twentyFourHour);
+ }else{
+ return window.moment(parsed).format('YYYY-MM-DD');
+ }
}
return `invalid ${fieldName} date `;
}
@@ -132,7 +142,7 @@
* @param typedDate - what the user has entered, such as '2023-01-23' or 'tomorrow'
* @returns the parsed date string. Includes "invalid" if {@code typedDate} was invalid.
*/
- function parseTypedDateForDisplayUsingFutureDate(fieldName: 'start' | 'scheduled' | 'due' | 'done', typedDate: string): string {
+ function parseTypedDateForDisplayUsingFutureDate(fieldName: 'start' | 'scheduled' | 'due' | 'done' | 'reminder', typedDate: string): string {
return parseTypedDateForDisplay(
fieldName,
typedDate,
@@ -158,7 +168,7 @@
}
$: accesskey = (key: string) => withAccessKeys ? key : null;
- $: formIsValid = isDueDateValid && isRecurrenceValid && isScheduledDateValid && isStartDateValid && isDescriptionValid;
+ $: formIsValid = isDueDateValid && isRecurrenceValid && isScheduledDateValid && isStartDateValid && isReminderDateValid && isDescriptionValid;
$: isDescriptionValid = editableTask.description.trim() !== '';
$: {
@@ -179,6 +189,12 @@
isDueDateValid = !parsedDueDate.includes('invalid');
}
+ $: {
+ editableTask.reminderDate = doAutocomplete(editableTask.reminderDate);
+ parsedReminderDate = parseTypedDateForDisplayUsingFutureDate('reminder', editableTask.reminderDate);
+ isReminderDateValid = !parsedReminderDate.includes('invalid');
+ }
+
$: {
isRecurrenceValid = true;
if (!editableTask.recurrenceRule) {
@@ -190,6 +206,7 @@
startDate: null,
scheduledDate: null,
dueDate: null,
+ reminder: null,
})?.toText();
if (!recurrenceFromText) {
parsedRecurrence = 'invalid recurrence rule ';
@@ -239,6 +256,7 @@
? task.scheduledDate.format('YYYY-MM-DD')
: '',
dueDate: task.dueDate ? task.dueDate.format('YYYY-MM-DD') : '',
+ reminderDate: task.reminder? task.reminder!.time.format(TIME_FORMATS.twentyFourHour) : '',
doneDate: task.doneDate ? task.doneDate.format('YYYY-MM-DD') : '',
forwardOnly: true,
};
@@ -287,6 +305,9 @@
const dueDate = parseTypedDateForSaving(editableTask.dueDate);
+ // TODO Add tests for all the reminder handling inside the Edit modal tests
+ const reminderDate = parseTypedDateForSaving(editableTask.reminderDate);
+
let recurrence: Recurrence | null = null;
if (editableTask.recurrenceRule) {
recurrence = Recurrence.fromText({
@@ -294,6 +315,7 @@
startDate,
scheduledDate,
dueDate,
+ reminder: reminderDate ? parseMoment(reminderDate) : null,
});
}
@@ -321,6 +343,7 @@
startDate,
scheduledDate,
dueDate,
+ reminder: reminderDate ? parseMoment(reminderDate) : null,
doneDate: window
.moment(editableTask.doneDate, 'YYYY-MM-DD')
.isValid()
@@ -443,6 +466,21 @@
/>
{startDateSymbol} {@html parsedStartDate}
+
+
+
+ Re minder
+
+
+ {reminderDateSymbol} {@html parsedReminderDate}
+
diff --git a/tests/CustomMatchers/CustomMatchersForFilters.ts b/tests/CustomMatchers/CustomMatchersForFilters.ts
index c4c6c849a5..705f577a45 100644
--- a/tests/CustomMatchers/CustomMatchersForFilters.ts
+++ b/tests/CustomMatchers/CustomMatchersForFilters.ts
@@ -138,13 +138,17 @@ export function toMatchTask(filter: FilterOrErrorMessage, task: Task) {
const matches = filter.filterFunction!(task);
if (!matches) {
return {
- message: () => `unexpected failure to match task: ${task.toFileLineString()}`,
+ message: () => `unexpected failure to match
+task: "${task.toFileLineString()}"
+with filter: "${filter.instruction}"`,
pass: false,
};
}
return {
- message: () => `filter should not have matched task: ${task.toFileLineString()}`,
+ message: () => `filter should not have matched
+task: "${task.toFileLineString()}"
+with filter: "${filter.instruction}"`,
pass: true,
};
}
diff --git a/tests/CustomMatchers/CustomMatchersForTaskSerializer.ts b/tests/CustomMatchers/CustomMatchersForTaskSerializer.ts
index 2c9814cdbf..865d0cbede 100644
--- a/tests/CustomMatchers/CustomMatchersForTaskSerializer.ts
+++ b/tests/CustomMatchers/CustomMatchersForTaskSerializer.ts
@@ -80,6 +80,7 @@ function summarizeTaskDetails(t: TaskDetails | null): SummarizedTaskDetails | nu
dueDate: t.dueDate?.format(TaskRegularExpressions.dateFormat) ?? null,
doneDate: t.doneDate?.format(TaskRegularExpressions.dateFormat) ?? null,
recurrence: t.recurrence?.toText() ?? null,
+ reminder: t.reminder?.toString() ?? null,
};
}
@@ -103,6 +104,7 @@ function tryBuildTaskDetails(t: object): TaskDetails | null {
doneDate: null,
recurrence: null,
tags: [],
+ reminder: null,
...t,
};
if (!isTaskDetails(toReturn)) return null;
diff --git a/tests/Query.test.ts b/tests/Query.test.ts
index bcc93a1056..f829d87e4f 100644
--- a/tests/Query.test.ts
+++ b/tests/Query.test.ts
@@ -50,6 +50,7 @@ describe('Query parsing', () => {
'has done date',
'has due date',
'has happens date',
+ 'has reminder date',
'has scheduled date',
'has start date',
'has tags',
@@ -63,6 +64,7 @@ describe('Query parsing', () => {
'no due date',
'no due date',
'no happens date',
+ 'no reminder date',
'no scheduled date',
'no start date',
'no tags',
@@ -79,6 +81,13 @@ describe('Query parsing', () => {
'priority is none',
'recurrence does not include wednesday',
'recurrence includes wednesday',
+ 'reminder after 2021-12-27',
+ 'reminder after yesterday',
+ 'reminder before 2021-12-27',
+ 'reminder date is invalid',
+ 'reminder in 2021-12-27 2021-12-29',
+ 'reminder on 2021-12-27',
+ 'reminder this week',
'scheduled after 2021-12-27',
'scheduled before 2021-12-27',
'scheduled date is invalid',
@@ -169,6 +178,8 @@ describe('Query parsing', () => {
'sort by priority reverse',
'sort by priority',
'sort by recurring',
+ 'sort by reminder reverse',
+ 'sort by reminder',
'sort by scheduled reverse',
'sort by scheduled',
'sort by start reverse',
@@ -224,6 +235,8 @@ describe('Query parsing', () => {
'group by recurrence reverse',
'group by recurring',
'group by recurring reverse',
+ 'group by reminder',
+ 'group by reminder reverse',
'group by root',
'group by root reverse',
'group by scheduled',
@@ -264,6 +277,7 @@ describe('Query parsing', () => {
'hide edit button',
'hide priority',
'hide recurrence rule',
+ 'hide reminder date',
'hide scheduled date',
'hide start date',
'hide task count',
@@ -280,6 +294,7 @@ describe('Query parsing', () => {
'show edit button',
'show priority',
'show recurrence rule',
+ 'show reminder date',
'show scheduled date',
'show created date',
'show start date',
@@ -342,6 +357,7 @@ describe('Query', () => {
recurrence: null,
blockLink: '',
tags: [],
+ reminder: null,
originalMarkdown: '',
scheduledDateIsInferred: false,
createdDate: null,
@@ -360,6 +376,7 @@ describe('Query', () => {
recurrence: null,
blockLink: '',
tags: [],
+ reminder: null,
originalMarkdown: '',
scheduledDateIsInferred: false,
createdDate: null,
@@ -425,6 +442,21 @@ describe('Query', () => {
],
},
],
+ [
+ 'by reminder date presence',
+ {
+ filters: ['has reminder date'],
+ tasks: [
+ '- [ ] task 1',
+ '- [ ] task 2 🛫 2022-04-20 ⏳ 2022-04-20 ⏰️ 2022-04-20',
+ '- [ ] task 3 ⏰️ 2022-04-20',
+ ],
+ expectedResult: [
+ '- [ ] task 2 🛫 2022-04-20 ⏳ 2022-04-20 ⏰️ 2022-04-20',
+ '- [ ] task 3 ⏰️ 2022-04-20',
+ ],
+ },
+ ],
[
'by due date absence',
{
@@ -461,6 +493,18 @@ describe('Query', () => {
expectedResult: ['- [ ] task 1'],
},
],
+ [
+ 'by reminder date absence',
+ {
+ filters: ['no reminder date'],
+ tasks: [
+ '- [ ] task 1',
+ '- [ ] task 2 🛫 2022-04-20 ⏳ 2022-04-20 ⏰️ 2022-04-20',
+ '- [ ] task 3 ⏰️ 2022-04-20',
+ ],
+ expectedResult: ['- [ ] task 1'],
+ },
+ ],
[
'by start date (before)',
{
diff --git a/tests/Query/Filter/ReminderDateField.test.ts b/tests/Query/Filter/ReminderDateField.test.ts
new file mode 100644
index 0000000000..61c6fe9ad1
--- /dev/null
+++ b/tests/Query/Filter/ReminderDateField.test.ts
@@ -0,0 +1,155 @@
+/**
+ * @jest-environment jsdom
+ */
+import moment from 'moment';
+import { ReminderDateField } from '../../../src/Query/Filter/ReminderDateField';
+import { TaskBuilder } from '../../TestingTools/TaskBuilder';
+import { expectTaskComparesAfter, expectTaskComparesBefore } from '../../CustomMatchers/CustomMatchersForSorting';
+import type { FilterOrErrorMessage } from '../../../src/Query/Filter/Filter';
+import { testFilter } from '../../TestingTools/FilterTestHelpers';
+
+window.moment = moment;
+
+function testTaskFilterForTaskWithReminderDate(
+ filter: FilterOrErrorMessage,
+ reminderDateTime: string | null,
+ expected: boolean,
+) {
+ const builder = new TaskBuilder();
+ testFilter(filter, builder.reminder(reminderDateTime), expected);
+}
+
+describe('reminder date', () => {
+ afterAll(() => {
+ jest.useRealTimers();
+ });
+
+ it('by reminder date (before)', () => {
+ // Arrange
+ const filter = new ReminderDateField().createFilterOrErrorMessage('reminder before 2022-04-20 11:45');
+
+ // Act, Assert
+ testTaskFilterForTaskWithReminderDate(filter, null, false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-15', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-19 23:59', true);
+
+ // Filter matches whole day, even though it was supplied with a time, so reminders
+ // with any time on the filter's day should match.
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20', false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 11:44', false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 11:45', false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 11:46', false);
+
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-21', false);
+ });
+
+ it('by reminder date (after)', () => {
+ // Arrange
+ const filter = new ReminderDateField().createFilterOrErrorMessage('reminder after 2022-04-19 11:45');
+
+ // Act, Assert
+ testTaskFilterForTaskWithReminderDate(filter, null, false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-15', false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-18 23:59', false);
+
+ // Filter matches whole day, even though it was supplied with a time, so reminders
+ // with any time on the filter's day should match.
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 11:44', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 11:45', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 11:46', true);
+
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-21', true);
+ });
+
+ it('by reminder date (on) - with filter containing date only', () => {
+ // Arrange
+ const filter = new ReminderDateField().createFilterOrErrorMessage('reminder on 2022-04-20');
+
+ // Act, Assert
+ testTaskFilterForTaskWithReminderDate(filter, null, false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-15', false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 09:15', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-25', false);
+ });
+
+ it('by reminder date (on) - with filter containing date and time', () => {
+ // Arrange
+ const filter = new ReminderDateField().createFilterOrErrorMessage('reminder on 2022-04-20 15:43');
+
+ // Act, Assert
+ testTaskFilterForTaskWithReminderDate(filter, null, false);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-15', false);
+
+ // Filter matches whole day, even though it was supplied with a time, so reminders
+ // with any time on the filter's day should match.
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 09:15', true);
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-20 15:43', true);
+
+ testTaskFilterForTaskWithReminderDate(filter, '2022-04-25', false);
+ });
+});
+
+describe('explain reminder date queries', () => {
+ it('should explain explicit date', () => {
+ const filterOrMessage = new ReminderDateField().createFilterOrErrorMessage('reminder before 2023-01-02');
+ expect(filterOrMessage).toHaveExplanation('reminder date is before 2023-01-02 (Monday 2nd January 2023)');
+ });
+
+ it('should show that times in reminder filters are ignored', () => {
+ const filterOrMessage = new ReminderDateField().createFilterOrErrorMessage('reminder on 2023-01-02 15:43');
+ expect(filterOrMessage).toHaveExplanation('reminder date is on 2023-01-02 (Monday 2nd January 2023)');
+ });
+
+ it('implicit "on" gets added to explanation', () => {
+ const filterOrMessage = new ReminderDateField().createFilterOrErrorMessage('reminder 2023-01-02');
+ expect(filterOrMessage).toHaveExplanation('reminder date is on 2023-01-02 (Monday 2nd January 2023)');
+ });
+});
+
+describe('sorting by reminder', () => {
+ it('supports Field sorting methods correctly', () => {
+ const field = new ReminderDateField();
+ expect(field.supportsSorting()).toEqual(true);
+ });
+
+ // These are minimal tests just to confirm basic behaviour is set up for this field.
+ // Thorough testing is done in DueDateField.test.ts.
+
+ // Because Reminder is the first field to support times, we do need to test
+ // that sorting takes account of differences in reminder time.
+ const date1 = new TaskBuilder().reminder('2021-01-12').build();
+ const date2 = new TaskBuilder().reminder('2022-12-23').build();
+ const date3 = new TaskBuilder().reminder('2022-12-23 09:27').build();
+ const date4 = new TaskBuilder().reminder('2022-12-23 13:59').build();
+
+ it('sort by reminder', () => {
+ const sorter = new ReminderDateField().createNormalSorter();
+ expectTaskComparesBefore(sorter, date1, date2);
+ expectTaskComparesBefore(sorter, date2, date3);
+ expectTaskComparesBefore(sorter, date3, date4);
+ });
+
+ it('sort by reminder reverse', () => {
+ expectTaskComparesAfter(new ReminderDateField().createReverseSorter(), date1, date2);
+ });
+});
+
+describe('grouping by reminder date', () => {
+ it('supports Field grouping methods correctly', () => {
+ expect(new ReminderDateField()).toSupportGroupingWithProperty('reminder');
+ });
+
+ it('group by reminder date', () => {
+ // Arrange
+ const grouper = new ReminderDateField().createNormalGrouper();
+ const taskWithDate = new TaskBuilder().reminder('1970-01-01').build();
+ const taskWithoutDate = new TaskBuilder().build();
+
+ // Assert
+ expect(grouper.grouper(taskWithDate)).toEqual(['1970-01-01 Thursday']);
+ expect(grouper.grouper(taskWithoutDate)).toEqual(['No reminder date']);
+ });
+});
diff --git a/tests/Recurrence.test.ts b/tests/Recurrence.test.ts
index 9892613774..cb5a33f821 100644
--- a/tests/Recurrence.test.ts
+++ b/tests/Recurrence.test.ts
@@ -2,7 +2,9 @@
* @jest-environment jsdom
*/
import moment from 'moment';
+import { parseMoment } from '../src/Reminders/Reminder';
import { Recurrence } from '../src/Recurrence';
+import { TIME_FORMATS } from '../src/Config/Settings';
import { RecurrenceBuilder } from './TestingTools/RecurrenceBuilder';
jest.mock('obsidian');
@@ -16,6 +18,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: null,
+ reminder: null,
});
// Act
@@ -26,6 +29,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: null,
+ reminder: null,
});
});
@@ -36,6 +40,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: moment('2022-01-31').startOf('day'),
+ reminder: null,
});
// Act
@@ -54,6 +59,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: moment('2022-01-31').startOf('day'),
+ reminder: null,
});
// Act
@@ -72,6 +78,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: moment('2023-12-31').startOf('day'),
+ reminder: null,
});
// Act
@@ -90,6 +97,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: moment('2024-02-29').startOf('day'),
+ reminder: null,
});
// Act
@@ -108,6 +116,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: moment('2020-03-31').startOf('day'),
+ reminder: null,
});
// Act
@@ -126,6 +135,7 @@ describe('Recurrence', () => {
startDate: null,
scheduledDate: null,
dueDate: moment('2020-01-31').startOf('day'),
+ reminder: null,
});
// Act
@@ -148,6 +158,7 @@ describe('Recurrence - with invalid dates in tasks', () => {
startDate: null,
scheduledDate: null,
dueDate: moment('2022-02-30').startOf('day'), // 30th February: invalid date
+ reminder: null,
});
// Assert
@@ -168,6 +179,7 @@ describe('Recurrence - with invalid dates in tasks', () => {
startDate: null,
scheduledDate: moment('2022-02-30').startOf('day'), // 30th February: invalid date
dueDate: moment('2022-02-27').startOf('day'),
+ reminder: null,
});
// Act
@@ -231,4 +243,44 @@ describe('identicalTo', () => {
expect(date1Recurrence?.identicalTo(date1Recurrence)).toBe(true);
expect(date1Recurrence?.identicalTo(date2Recurrence)).toBe(false);
});
+
+ it('differing only in reminder', () => {
+ const date1Recurrence = new RecurrenceBuilder().reminders('2021-10-21').build();
+
+ const date2Recurrence = new RecurrenceBuilder().reminders('1998-03-13').build();
+
+ const nullRecurrence = new RecurrenceBuilder().reminders('').build();
+
+ expect(date1Recurrence?.identicalTo(date1Recurrence)).toBe(true);
+ expect(date1Recurrence?.identicalTo(date2Recurrence)).toBe(false);
+ expect(date1Recurrence?.identicalTo(nullRecurrence)).toBe(false);
+ expect(nullRecurrence?.identicalTo(date1Recurrence)).toBe(false);
+ });
+});
+
+describe('Recurrence - with reminders', () => {
+ it('creates a recurring instance with single 24h reminders', () => {
+ // Arrange
+ const originalReminder = parseMoment(moment('2021-06-20 13:00', TIME_FORMATS.twentyFourHour));
+ const originalReminderAsString = originalReminder.toString();
+ const recurrence = Recurrence.fromText({
+ recurrenceRuleText: 'every week',
+ startDate: null,
+ scheduledDate: null,
+ dueDate: null,
+ reminder: originalReminder,
+ });
+
+ // Act
+ const next = recurrence!.next();
+
+ // Assert
+ expect(
+ next!.reminder!.isSame(parseMoment(moment('2021-06-27 13:00', TIME_FORMATS.twentyFourHour))),
+ ).toStrictEqual(true);
+ expect(next!.reminder!.toString()).toStrictEqual('2021-06-27 13:00');
+
+ // Confirm that the original date has not been modified
+ expect(originalReminder.toString()).toStrictEqual(originalReminderAsString);
+ });
});
diff --git a/tests/Reminder/Notifications.test.ts b/tests/Reminder/Notifications.test.ts
new file mode 100644
index 0000000000..1a8f116cf8
--- /dev/null
+++ b/tests/Reminder/Notifications.test.ts
@@ -0,0 +1,37 @@
+/**
+ * @jest-environment jsdom
+ */
+jest.mock('obsidian');
+import moment from 'moment';
+import { Reminder, ReminderType, parseMoment } from '../../src/Reminders/Reminder';
+//import { TaskNotification } from '../../src/Reminders/Notification';
+window.moment = moment;
+
+describe('Notifications ', () => {
+ it('testing parsing Moment() datetime', () => {
+ const reminder = moment('2023-04-30 11:44 am');
+
+ expect(parseMoment(reminder)).toStrictEqual(new Reminder(moment('2023-04-30 11:44 am'), ReminderType.DateTime));
+ });
+
+ // it('testing shouldNotifiy', () => {
+ // const curTime = moment('2023-04-30 11:50 am');
+ // const dailyReminder = parseMoment(moment('2023-04-30'));
+ // const timeReminder = parseMoment(moment('2023-04-30 11:50 am'));
+ // const oldReminder = parseMoment(moment('2023-04-30 11:49 am'));
+ // const futureRinder = parseMoment(moment('2023-04-30 11:51 am'));
+
+ // expect(TaskNotification.shouldNotifiy(dailyReminder, curTime, curTime)).toBe(true);
+ // expect(TaskNotification.shouldNotifiy(timeReminder, curTime, curTime)).toBe(true);
+ // expect(TaskNotification.shouldNotifiy(oldReminder, curTime, curTime)).toBe(false);
+ // expect(TaskNotification.shouldNotifiy(futureRinder, curTime, curTime)).toBe(false);
+
+ // /* Fails: TypeError: Class extends value undefined is not a constructor or null
+ // > 145 | class ObsidianNotificationModal extends Modal {
+ // | ^
+ // at Object. (src/Reminders/Notification.ts:145:41)
+ // at Object. (tests/Reminder/Notifications.test.ts:8:1)
+
+ // */
+ // });
+});
diff --git a/tests/Reminder/Reminder.test.ts b/tests/Reminder/Reminder.test.ts
new file mode 100644
index 0000000000..627b1552e4
--- /dev/null
+++ b/tests/Reminder/Reminder.test.ts
@@ -0,0 +1,54 @@
+/**
+ * @jest-environment jsdom
+ */
+jest.mock('obsidian');
+import moment from 'moment';
+import { TIME_FORMATS, resetSettings } from '../../src/Config/Settings';
+import { Reminder, ReminderType, parseDateTime, parseMoment } from '../../src/Reminders/Reminder';
+import { fromLine } from '../TestHelpers';
+
+window.moment = moment;
+
+function checkParsedDateTime(input: string, output: string) {
+ const dateTime = parseDateTime(input);
+ expect(dateTime).not.toBeNull();
+ expect(dateTime!.toString()).toEqual(output);
+}
+
+describe('should parse Moment() dates & times as reminder: ', () => {
+ it('test Moment() datetime 24hr', () => {
+ const reminder = moment('2023-04-30 13:45', TIME_FORMATS.twentyFourHour);
+ expect(parseMoment(reminder)).toStrictEqual(
+ new Reminder(moment('2023-04-30 13:45', TIME_FORMATS.twentyFourHour), ReminderType.DateTime),
+ );
+ });
+
+ it('test Moment() date', () => {
+ const reminder = moment('2023-04-30');
+ expect(parseMoment(reminder)).toStrictEqual(new Reminder(moment('2023-04-30'), ReminderType.Date));
+ });
+});
+
+describe('should parse dates & times string as reminder: ', () => {
+ afterEach(function () {
+ resetSettings();
+ });
+
+ it('test 24-hour format', () => {
+ checkParsedDateTime('2023-01-15', '2023-01-15');
+ checkParsedDateTime('2023-01-15 13:45', '2023-01-15 13:45');
+ checkParsedDateTime('2023-01-15 1:45 pm', '2023-01-15 01:45'); // the pm & leading 0 are ignored
+ });
+});
+
+describe('should parse task strings: ', () => {
+ afterEach(function () {
+ resetSettings();
+ });
+
+ it('valid task - in 24-hour format', () => {
+ const line = '- [ ] #task Reminder at 13:57 ⏰️ 2023-05-03 13:57';
+ const task = fromLine({ line: line });
+ expect(task.reminder).not.toBeNull();
+ });
+});
diff --git a/tests/Task.test.ts b/tests/Task.test.ts
index da22332b08..3c3e7bf047 100644
--- a/tests/Task.test.ts
+++ b/tests/Task.test.ts
@@ -1245,6 +1245,14 @@ describe('identicalTo', () => {
expect(lhs).toBeIdenticalTo(new TaskBuilder().tags([]));
expect(lhs).not.toBeIdenticalTo(new TaskBuilder().tags(['#stuff']));
});
+
+ it('should check reminders', () => {
+ const lhs = new TaskBuilder().reminder('2023-03-07 09:25 am');
+ expect(lhs).toBeIdenticalTo(new TaskBuilder().reminder('2023-03-07 09:25 am'));
+ expect(lhs).not.toBeIdenticalTo(new TaskBuilder().reminder(null));
+ expect(lhs).not.toBeIdenticalTo(new TaskBuilder().reminder('2023-03-07'));
+ expect(lhs).not.toBeIdenticalTo(new TaskBuilder().reminder('2023-03-07 09:27 am'));
+ });
});
describe('checking if task lists are identical', () => {
diff --git a/tests/TaskSerializer/DefaultTaskSerializer.test.ts b/tests/TaskSerializer/DefaultTaskSerializer.test.ts
index e9c1774b91..51b4168c4b 100644
--- a/tests/TaskSerializer/DefaultTaskSerializer.test.ts
+++ b/tests/TaskSerializer/DefaultTaskSerializer.test.ts
@@ -3,11 +3,12 @@
*/
import moment from 'moment';
import { Priority } from '../../src/Task';
-import type { Settings } from '../../src/Config/Settings';
+import { type Settings, TIME_FORMATS, resetSettings } from '../../src/Config/Settings';
import { DefaultTaskSerializer } from '../../src/TaskSerializer';
import { RecurrenceBuilder } from '../TestingTools/RecurrenceBuilder';
import { DEFAULT_SYMBOLS, type DefaultTaskSerializerSymbols } from '../../src/TaskSerializer/DefaultTaskSerializer';
import { TaskBuilder } from '../TestingTools/TaskBuilder';
+import { parseMoment } from '../../src/Reminders/Reminder';
jest.mock('obsidian');
window.moment = moment;
@@ -23,8 +24,15 @@ describe.each(symbolMap)("DefaultTaskSerializer with '$taskFormat' symbols", ({
const taskSerializer = new DefaultTaskSerializer(symbols);
const serialize = taskSerializer.serialize.bind(taskSerializer);
const deserialize = taskSerializer.deserialize.bind(taskSerializer);
- const { startDateSymbol, createdDateSymbol, recurrenceSymbol, scheduledDateSymbol, dueDateSymbol, doneDateSymbol } =
- symbols;
+ const {
+ startDateSymbol,
+ createdDateSymbol,
+ recurrenceSymbol,
+ scheduledDateSymbol,
+ dueDateSymbol,
+ doneDateSymbol,
+ reminderDateSymbol,
+ } = symbols;
describe('deserialize', () => {
it('should parse an empty string', () => {
@@ -65,7 +73,26 @@ describe.each(symbolMap)("DefaultTaskSerializer with '$taskFormat' symbols", ({
it('should parse tags', () => {
const description = ' #hello #world #task';
const taskDetails = deserialize(description);
- expect(taskDetails).toMatchTaskDetails({ tags: ['#hello', '#world', '#task'], description });
+ expect(taskDetails).toMatchTaskDetails({
+ tags: ['#hello', '#world', '#task'],
+ description,
+ });
+ });
+ });
+
+ describe('deserialize reminders', () => {
+ afterEach(function () {
+ resetSettings();
+ });
+
+ it('should parse a single 24h reminder', () => {
+ const times = ['2021-06-20 13:45', '2021-06-20 01:45', '2021-06-20'];
+ times.forEach((time) => {
+ const taskDetails = deserialize(`${reminderDateSymbol} ${time}`);
+ expect(taskDetails).toMatchTaskDetails({
+ reminder: parseMoment(moment(time, TIME_FORMATS.twentyFourHour)),
+ });
+ });
});
});
@@ -115,5 +142,25 @@ describe.each(symbolMap)("DefaultTaskSerializer with '$taskFormat' symbols", ({
const serialized = serialize(task);
expect(serialized).toEqual(' #hello #world #task');
});
+
+ it('should serialize a single reminder date', () => {
+ const serialized = serialize(new TaskBuilder().reminder('2021-06-20').description('').build());
+ expect(serialized).toEqual(` ${reminderDateSymbol} 2021-06-20`);
+ });
+ });
+
+ describe('serialize reminders', () => {
+ afterEach(function () {
+ resetSettings();
+ });
+
+ it('should serialize a single 24h reminder', () => {
+ const times = ['2021-06-20 13:45', '2021-06-20 01:45', '2021-06-20'];
+
+ times.forEach((time) => {
+ const serialized = serialize(new TaskBuilder().reminder(time).description('').build());
+ expect(serialized).toEqual(` ${reminderDateSymbol} ${time}`);
+ });
+ });
});
});
diff --git a/tests/TaskSerializer/TaskSerializer.test.ts b/tests/TaskSerializer/TaskSerializer.test.ts
index 3798049d4d..fa154f2e9e 100644
--- a/tests/TaskSerializer/TaskSerializer.test.ts
+++ b/tests/TaskSerializer/TaskSerializer.test.ts
@@ -15,9 +15,9 @@ window.moment = moment;
This file contains a tested, end-to-end example for implementing and using a
{@link TaskSerializer}.
- This file should also contain any {@link TaskSerializer} tests that should be tested
+ This file should also contain any {@link TaskSerializer} tests that should be tested
against all the {@link TaskSerializer}s defined in this repo. Tests that only
- apply to one should be housed in that serializer's specific test file
+ apply to one should be housed in that serializer's specific test file
*/
describe('TaskSerializer Example', () => {
@@ -68,6 +68,7 @@ describe('TaskSerializer Example', () => {
scheduledDate: null,
doneDate: null,
recurrence: null,
+ reminder: null,
};
}
@@ -103,8 +104,12 @@ describe('TaskSerializer Example', () => {
});
});
+ // TODO Figure out why this changed.
it('should parse a priority and description', () => {
- expect(ts.deserialize('1 Wobble')).toMatchTaskDetails({ priority: Priority.High, description: 'Wobble' });
+ expect(ts.deserialize('1 Wobble')).toMatchTaskDetails({
+ priority: Priority.High,
+ description: 'Wobble',
+ });
});
it('should parse a full task', () => {
diff --git a/tests/TestingTools/FilterTestHelpers.ts b/tests/TestingTools/FilterTestHelpers.ts
index 1babf70a1b..2047a142bb 100644
--- a/tests/TestingTools/FilterTestHelpers.ts
+++ b/tests/TestingTools/FilterTestHelpers.ts
@@ -25,9 +25,12 @@ export function testFilter(filter: FilterOrErrorMessage, taskBuilder: TaskBuilde
* @param expected true if the task should match the filter, and false otherwise.
*/
export function testTaskFilter(filter: FilterOrErrorMessage, task: Task, expected: boolean) {
- expect(filter.filterFunction).toBeDefined();
- expect(filter.error).toBeUndefined();
- expect(filter.filterFunction!(task)).toEqual(expected);
+ expect(filter).toBeValid();
+ if (expected) {
+ expect(filter).toMatchTask(task);
+ } else {
+ expect(filter).not.toMatchTask(task);
+ }
}
/**
diff --git a/tests/TestingTools/RecurrenceBuilder.ts b/tests/TestingTools/RecurrenceBuilder.ts
index bced56128c..940302b061 100644
--- a/tests/TestingTools/RecurrenceBuilder.ts
+++ b/tests/TestingTools/RecurrenceBuilder.ts
@@ -2,6 +2,7 @@
import type { Moment } from 'moment';
import { Recurrence } from '../../src/Recurrence';
import { DateParser } from '../../src/Query/DateParser';
+import { Reminder, parseDateTime } from '../../src/Reminders/Reminder';
/**
* A fluent class for creating Recurrence objects for tests.
@@ -20,6 +21,7 @@ export class RecurrenceBuilder {
private _startDate: Moment | null = null;
private _scheduledDate: Moment | null = null;
private _dueDate: Moment | null = null;
+ private _reminder: Reminder | null = null;
/**
* Build a Recurrence
@@ -38,6 +40,7 @@ export class RecurrenceBuilder {
startDate: this._startDate,
scheduledDate: this._scheduledDate,
dueDate: this._dueDate,
+ reminder: this._reminder,
}) as Recurrence;
}
@@ -61,6 +64,15 @@ export class RecurrenceBuilder {
return this;
}
+ public reminders(reminder: string): RecurrenceBuilder {
+ if (reminder.length > 0) {
+ this._reminder = parseDateTime(reminder);
+ } else {
+ this._reminder = null;
+ }
+ return this;
+ }
+
private static parseDate(date: string | null): Moment | null {
if (date) {
return DateParser.parseDate(date);
diff --git a/tests/TestingTools/TaskBuilder.ts b/tests/TestingTools/TaskBuilder.ts
index 0b5a781509..13fa886a5f 100644
--- a/tests/TestingTools/TaskBuilder.ts
+++ b/tests/TestingTools/TaskBuilder.ts
@@ -6,6 +6,7 @@ import type { Recurrence } from '../../src/Recurrence';
import { DateParser } from '../../src/Query/DateParser';
import { StatusConfiguration, StatusType } from '../../src/StatusConfiguration';
import { TaskLocation } from '../../src/TaskLocation';
+import { Reminder, parseDateTime } from '../../src/Reminders/Reminder';
/**
* A fluent class for creating tasks for tests.
@@ -39,6 +40,7 @@ export class TaskBuilder {
private _scheduledDate: Moment | null = null;
private _dueDate: Moment | null = null;
private _doneDate: Moment | null = null;
+ private _reminders: Reminder | null = null; // TODO Rename to singular
private _recurrence: Recurrence | null = null;
private _blockLink: string = '';
@@ -83,6 +85,7 @@ export class TaskBuilder {
recurrence: this._recurrence,
blockLink: this._blockLink,
tags: this._tags,
+ reminder: this._reminders,
originalMarkdown: '',
scheduledDateIsInferred: this._scheduledDateIsInferred,
});
@@ -194,6 +197,15 @@ export class TaskBuilder {
return this;
}
+ public reminder(reminder: string | null): TaskBuilder {
+ if (reminder) {
+ this._reminders = parseDateTime(reminder);
+ } else {
+ this._reminders = null;
+ }
+ return this;
+ }
+
/**
* See {@link RecurrenceBuilder} for easy construction of {@link Recurrence} objects in tests.
* @param recurrence