Skip to content

Commit

Permalink
feat: Add a button, with context menu, to "postpone" tasks (#2191)
Browse files Browse the repository at this point in the history
* feat: add a button for snoozing till tomorrow

* chore: add styles for the snooze button

* Merge "main" branch into "feat--snooze-button"

* chore(postpone button): rename snooze button to postpone button

* chore(postpone button): extract a postpone method to the TaskDate class

* feat: add the way to postpone task for a week, month or quarter

* fix: postpone the past dates

* fix: context menu position

* fix: decrease an icon size for the postpone button

* fix: decrease an icon size for the postpone button

---------

Co-authored-by: Clare Macrae <[email protected]>
  • Loading branch information
m0rtyn and claremacrae authored Nov 28, 2023
1 parent bb4c414 commit 16f6bae
Show file tree
Hide file tree
Showing 6 changed files with 125 additions and 4 deletions.
5 changes: 4 additions & 1 deletion src/Query/Query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class Query implements IQuery {
private _ignoreGlobalQuery: boolean = false;

private readonly hideOptionsRegexp =
/^(hide|show) (task count|backlink|priority|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|urgency|tags)/i;
/^(hide|show) (task count|backlink|priority|created date|start date|scheduled date|done date|due date|recurrence rule|edit button|postpone button|urgency|tags)/i;
private readonly shortModeRegexp = /^short/i;
private readonly explainQueryRegexp = /^explain/i;
private readonly ignoreGlobalQueryRegexp = /^ignore global query/i;
Expand Down Expand Up @@ -295,6 +295,9 @@ Problem line: "${line}"`;
case 'backlink':
this._layoutOptions.hideBacklinks = hide;
break;
case 'postpone button':
this._layoutOptions.hidePostponeButton = hide;
break;
case 'priority':
this._layoutOptions.hidePriority = hide;
break;
Expand Down
95 changes: 93 additions & 2 deletions src/QueryRenderer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { EventRef, MarkdownPostProcessorContext } from 'obsidian';
import { App, Keymap, MarkdownRenderChild, MarkdownRenderer, Plugin, TFile } from 'obsidian';
import { App, Keymap, MarkdownRenderChild, MarkdownRenderer, Menu, MenuItem, Notice, Plugin, TFile } from 'obsidian';
import type { unitOfTime } from 'moment';
import { State } from './Cache';
import { GlobalFilter } from './Config/GlobalFilter';
import { GlobalQuery } from './Config/GlobalQuery';
Expand All @@ -11,11 +12,12 @@ import { explainResults, getQueryForQueryRenderer } from './lib/QueryRendererHel
import type { GroupDisplayHeading } from './Query/GroupDisplayHeading';
import type { QueryResult } from './Query/QueryResult';
import type { TaskGroups } from './Query/TaskGroups';
import type { Task } from './Task';
import { Task } from './Task';
import { TaskLayout } from './TaskLayout';
import { TaskLineRenderer } from './TaskLineRenderer';
import { TaskModal } from './TaskModal';
import type { TasksEvents } from './TasksEvents';
import { TasksDate } from './Scripting/TasksDate';

export class QueryRenderer {
private readonly app: App;
Expand Down Expand Up @@ -232,8 +234,14 @@ class QueryRenderChild extends MarkdownRenderChild {
const footnotes = listItem.querySelectorAll('[data-footnote-id]');
footnotes.forEach((footnote) => footnote.remove());

const shortMode = this.query.layoutOptions.shortMode;

const extrasSpan = listItem.createSpan('task-extras');

if (!this.query.layoutOptions.hidePostponeButton) {
this.addPostponeButton(extrasSpan, task, shortMode);
}

if (!this.query.layoutOptions.hideUrgency) {
this.addUrgency(extrasSpan, task);
}
Expand All @@ -247,6 +255,13 @@ class QueryRenderChild extends MarkdownRenderChild {
this.addEditButton(extrasSpan, task);
}

// NEW
// if (!this.query.layoutOptions.hideSnoozeButton) {
// this.addUnSnoozeButton(extrasSpan, task, shortMode);
// this.addSnoozeButton1Day(extrasSpan, task, shortMode);
// this.addSnoozeButton3Days(extrasSpan, task, shortMode);
// }

taskList.appendChild(listItem);
}

Expand Down Expand Up @@ -383,6 +398,45 @@ class QueryRenderChild extends MarkdownRenderChild {
}
}

private addPostponeButton(listItem: HTMLElement, task: Task, shortMode: boolean) {
const button = listItem.createEl('button', {
attr: {
id: 'postpone-button',
title: 'ℹ️ Postpone the task (right-click for more options)',
},
});

const classNames = shortMode ? ['internal-button', 'internal-button-short-mode'] : ['internal-button'];
button.addClasses(classNames);
const buttonText = shortMode ? ' ⏩' : ' ⏩ Postpone';
button.setText(buttonText);

button.addEventListener('click', () => this.getOnClickCallback(task, button, 'days'));

/** Open a context menu on right-click.
* Give a choice of postponing for a week, month, or quarter.
*/
button.addEventListener('contextmenu', async (ev: MouseEvent) => {
const menu = new Menu();
const commonTitle = 'Postpone for';

const getMenuItemCallback = (item: MenuItem, timeUnit: unitOfTime.DurationConstructor, amount = 1) => {
const amountOrArticle = amount > 1 ? amount : 'a';
item.setTitle(`${commonTitle} ${amountOrArticle} ${timeUnit}`).onClick(() =>
this.getOnClickCallback(task, button, timeUnit, amount),
);
};

menu.addItem((item) => getMenuItemCallback(item, 'days', 2));
menu.addItem((item) => getMenuItemCallback(item, 'days', 3));
menu.addItem((item) => getMenuItemCallback(item, 'week'));
menu.addItem((item) => getMenuItemCallback(item, 'weeks', 2));
menu.addItem((item) => getMenuItemCallback(item, 'month'));

menu.showAtPosition({ x: ev.clientX, y: ev.clientY });
});
}

private addTaskCount(content: HTMLDivElement, queryResult: QueryResult) {
if (!this.query.layoutOptions.hideTaskCount) {
content.createDiv({
Expand Down Expand Up @@ -417,4 +471,41 @@ class QueryRenderChild extends MarkdownRenderChild {
}
return groupingRules.join(',');
}

private async getOnClickCallback(
task: Task,
button: HTMLButtonElement,
timeUnit: unitOfTime.DurationConstructor = 'days',
amount = 1,
) {
const errorMessage = '⚠️ Postponement requires a date: due or scheduled.';
if (!task.dueDate && !task.scheduledDate) return new Notice(errorMessage, 10000);
const scheduledDateOrNull = task.scheduledDate ? 'scheduledDate' : null;
const dateTypeToUpdate = task.dueDate ? 'dueDate' : scheduledDateOrNull;
if (dateTypeToUpdate === null) return;

const dateToUpdate = task[dateTypeToUpdate];
const postponedDate = new TasksDate(dateToUpdate).postpone(timeUnit, amount);
const newTasks = new Task({ ...task, [dateTypeToUpdate]: postponedDate });

await replaceTaskWithTasks({
originalTask: task,
newTasks,
});

const postponedDateString = postponedDate?.format('DD MMM YYYY');
this.onPostponeSuccessCallback(button, dateTypeToUpdate, postponedDateString);
}

private onPostponeSuccessCallback(
button: HTMLButtonElement,
updatedDateType: 'dueDate' | 'scheduledDate',
postponedDateString: string,
) {
// Disable the button to prevent update error due to the task not being reloaded yet.
button.disabled = true;
button.setAttr('title', 'You can perform this action again after reloading the file.');
new Notice(`Task's ${updatedDateType} postponed untill ${postponedDateString}`, 5000);
this.events.triggerRequestCacheUpdate(this.render.bind(this));
}
}
17 changes: 16 additions & 1 deletion src/Scripting/TasksDate.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { DurationInputArg2, Moment } from 'moment';
import type { DurationInputArg2, Moment, unitOfTime } from 'moment';
import { Notice } from 'obsidian';
import { TaskRegularExpressions } from '../Task';
import { PropertyCategory } from '../lib/PropertyCategory';

Expand Down Expand Up @@ -109,4 +110,18 @@ export class TasksDate {
const unit = words[1] as DurationInputArg2; // day, days, weeks, month, year
return earlier ? now.subtract(multiplier, unit) : now.add(multiplier, unit);
}

public postpone(unitOfTime: unitOfTime.DurationConstructor = 'days', amount: number = 1) {
if (!this._date) throw new Notice('Cannot postpone a null date');

const today = window.moment().startOf('day');
// According to the moment.js docs, isBefore is not stable so we use !isSameOrAfter: https://momentjs.com/docs/#/query/is-before/
const isDateBeforeToday = !this._date.isSameOrAfter(today, 'day');

if (isDateBeforeToday) {
return today.add(amount, unitOfTime);
}

return this._date.add(amount, unitOfTime);
}
}
2 changes: 2 additions & 0 deletions src/TaskLayout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
* See applyOptions below when adding options here.
*/
export class LayoutOptions {
hidePostponeButton: boolean = false;
hideTaskCount: boolean = false;
hideBacklinks: boolean = false;
hidePriority: boolean = false;
Expand Down Expand Up @@ -94,6 +95,7 @@ export class TaskLayout {
[this.options.hideUrgency, 'urgency'],
[this.options.hideBacklinks, 'backlinks'],
[this.options.hideEditButton, 'edit-button'],
[this.options.hidePostponeButton, 'postpone-button'],
];
for (const [hide, component] of componentsToGenerateClassesOnly) {
this.generateHiddenClassForTaskList(hide, component);
Expand Down
9 changes: 9 additions & 0 deletions styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,15 @@
text-decoration: none;
}

.internal-button {
background-color: transparent;
padding: 0;
font-size: var(--font-text-size);
background-color: transparent;
display: contents;
cursor: pointer;
}

.tasks-list-text {
position: relative;
}
Expand Down
1 change: 1 addition & 0 deletions tests/TaskLayout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ describe('TaskLayout tests', () => {
tasks-layout-hide-tags
tasks-layout-hide-backlinks
tasks-layout-hide-edit-button
tasks-layout-hide-postpone-button
tasks-layout-short-mode"
`);
});
Expand Down

0 comments on commit 16f6bae

Please sign in to comment.