Add Pomodoro session deletion

This commit is contained in:
callumalpass 2026-05-18 02:33:53 +10:00
parent cbe2b9acd6
commit 340f629226
6 changed files with 318 additions and 5 deletions

View file

@ -104,6 +104,7 @@ Example:
## Fixed
- ([#1147](https://github.com/callumalpass/tasknotes/issues/1147)) Added a delete action for recent Pomodoro statistics sessions, so erroneous Pomodoro history can be removed without editing plugin storage or daily-note frontmatter manually. Thanks to @RumiaKitinari for reporting this and @BrucePlumb for the follow-up.
- ([#1150](https://github.com/callumalpass/tasknotes/issues/1150)) Kept recurring tasks' due dates visible in Calendar Bases views, and showed their scheduled date when recurrence instances are hidden. Thanks to @thomatino for reporting this and @BrucePlumb for the follow-up.
- ([#1159](https://github.com/callumalpass/tasknotes/issues/1159)) Softened Calendar time-entry styling so recorded time blocks no longer visually outweigh task events. Thanks to @hatespinach for reporting this.
- ([#1191](https://github.com/callumalpass/tasknotes/issues/1191)) Aligned the task-card and relationships widgets inside notes with Obsidian's editor margins, including readable-line-width layouts. Thanks to @minchinweb for reporting this.

View file

@ -421,6 +421,14 @@ export const en: TranslationTree = {
recents: {
empty: "No sessions recorded yet",
duration: "{minutes} min",
delete: "Delete session",
deleteAria: "Delete Pomodoro session",
deleteConfirmTitle: "Delete Pomodoro session?",
deleteConfirmMessage:
"This removes the session from Pomodoro history. Existing task time entries are not changed.",
deleteConfirmButton: "Delete",
deleteSuccess: "Pomodoro session deleted",
deleteNotFound: "Pomodoro session was not found",
status: {
completed: "Completed",
interrupted: "Interrupted",

View file

@ -1102,15 +1102,35 @@ export class PomodoroService {
await this.saveHistoryToDailyNotes(history);
} else {
// Default plugin storage
const data = (await this.plugin.loadData()) || {};
data.pomodoroHistory = history;
await this.plugin.saveData(data);
await this.savePluginHistory(history);
}
} catch (error) {
console.error("Failed to save session history:", error);
}
}
async deleteSessionFromHistory(session: PomodoroSessionHistory): Promise<boolean> {
let deleted = false;
try {
const pluginHistory = await this.loadPluginHistory();
const filteredPluginHistory = pluginHistory.filter((entry) => entry.id !== session.id);
if (filteredPluginHistory.length !== pluginHistory.length) {
await this.savePluginHistory(filteredPluginHistory);
deleted = true;
}
if (this.plugin.settings.pomodoroStorageLocation === "daily-notes") {
deleted = (await this.deleteSessionFromDailyNote(session)) || deleted;
}
} catch (error) {
console.error("Failed to delete pomodoro session:", error);
}
return deleted;
}
async addSessionToHistory(session: PomodoroSession): Promise<void> {
if (!session.endTime) {
console.warn("Cannot add session to history without end time");
@ -1188,6 +1208,12 @@ export class PomodoroService {
return Array.isArray(pluginHistory) ? pluginHistory : [];
}
private async savePluginHistory(history: PomodoroSessionHistory[]): Promise<void> {
const data = (await this.plugin.loadData()) || {};
data.pomodoroHistory = history;
await this.plugin.saveData(data);
}
private async loadPluginHistoryForDateKey(dateKey: string): Promise<PomodoroSessionHistory[]> {
return filterPomodoroSessionsByDateKey(await this.loadPluginHistory(), dateKey);
}
@ -1391,6 +1417,47 @@ export class PomodoroService {
}
}
private async deleteSessionFromDailyNote(session: PomodoroSessionHistory): Promise<boolean> {
try {
if (!appHasDailyNotesPluginLoaded()) {
return false;
}
const sessionDateKey = getPomodoroSessionDateKey(session);
if (!sessionDateKey) {
return false;
}
const sessionDate = parseDateToLocal(sessionDateKey);
const dailyNoteMoment = getDailyNoteMoment(sessionDate);
const dailyNote = getDailyNote(dailyNoteMoment, getAllDailyNotes());
if (!dailyNote) {
return false;
}
const pomodoroField = this.plugin.fieldMapper.toUserField("pomodoros");
let deleted = false;
await this.plugin.app.fileManager.processFrontMatter(dailyNote, (frontmatter) => {
const existingSessions = Array.isArray(frontmatter[pomodoroField])
? frontmatter[pomodoroField].filter(isPomodoroSessionHistory)
: [];
const filteredSessions = existingSessions.filter((entry) => entry.id !== session.id);
if (filteredSessions.length !== existingSessions.length) {
frontmatter[pomodoroField] = filteredSessions;
deleted = true;
}
});
return deleted;
} catch (error) {
console.error(`Failed to delete pomodoro session from daily note:`, error);
return false;
}
}
/**
* Update a specific daily note with pomodoro sessions
*/

View file

@ -1,8 +1,9 @@
import { ItemView, WorkspaceLeaf, Setting, setIcon, setTooltip } from "obsidian";
import { ItemView, Menu, Notice, WorkspaceLeaf, Setting, setIcon, setTooltip } from "obsidian";
import { format, startOfWeek, endOfWeek } from "date-fns";
import type { Day } from "date-fns";
import TaskNotesPlugin from "../main";
import { POMODORO_STATS_VIEW_TYPE, PomodoroHistoryStats, PomodoroSessionHistory } from "../types";
import { showConfirmationModal } from "../modals/ConfirmationModal";
import {
getTodayLocal,
createUTCDateFromLocalCalendarDate,
@ -238,6 +239,10 @@ export class PomodoroStatsView extends ItemView {
const sessionEl = container.createDiv({
cls: "pomodoro-session-item pomodoro-stats-view__session-item",
});
this.registerDomEvent(sessionEl, "contextmenu", (event: MouseEvent) => {
event.preventDefault();
this.showSessionContextMenu(event, session);
});
const dateEl = sessionEl.createSpan({
cls: "session-date pomodoro-stats-view__session-date",
@ -278,6 +283,62 @@ export class PomodoroStatsView extends ItemView {
const taskName = session.taskPath.split("/").pop()?.replace(".md", "") || "";
taskEl.textContent = taskName;
}
const deleteButton = sessionEl.createEl("button", {
cls: "pomodoro-stats-view__session-delete-button",
attr: {
type: "button",
"aria-label": this.t("views.pomodoroStats.recents.deleteAria"),
},
});
setIcon(deleteButton, "trash-2");
setTooltip(deleteButton, this.t("views.pomodoroStats.recents.delete"), {
placement: "top",
});
this.registerDomEvent(deleteButton, "click", (event: MouseEvent) => {
event.preventDefault();
event.stopPropagation();
void this.confirmDeleteSession(session);
});
}
}
private showSessionContextMenu(event: MouseEvent, session: PomodoroSessionHistory): void {
const menu = new Menu();
menu.addItem((item) => {
item.setTitle(this.t("views.pomodoroStats.recents.delete"));
item.setIcon("trash");
item.onClick(() => {
void this.confirmDeleteSession(session);
});
});
menu.showAtMouseEvent(event);
}
private async confirmDeleteSession(session: PomodoroSessionHistory): Promise<void> {
const confirmed = await showConfirmationModal(this.plugin.app, {
title: this.t("views.pomodoroStats.recents.deleteConfirmTitle"),
message: this.t("views.pomodoroStats.recents.deleteConfirmMessage"),
confirmText: this.t("views.pomodoroStats.recents.deleteConfirmButton"),
cancelText: this.t("common.cancel"),
isDestructive: true,
});
if (!confirmed || !this.plugin.pomodoroService) {
return;
}
const deleted = await this.plugin.pomodoroService.deleteSessionFromHistory(session);
new Notice(
this.t(
deleted
? "views.pomodoroStats.recents.deleteSuccess"
: "views.pomodoroStats.recents.deleteNotFound"
)
);
if (deleted) {
await this.refreshStats();
}
}

View file

@ -244,7 +244,7 @@
/* Session Item Container */
.tasknotes-plugin .pomodoro-stats-view__session-item {
display: grid;
grid-template-columns: auto auto auto 1fr;
grid-template-columns: auto auto auto 1fr auto;
gap: var(--tn-spacing-md);
align-items: center;
padding: var(--tn-spacing-md);
@ -308,6 +308,27 @@
white-space: nowrap;
}
.tasknotes-plugin .pomodoro-stats-view__session-delete-button {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
padding: 0;
color: var(--tn-text-muted);
background: transparent;
border: 1px solid transparent;
border-radius: var(--tn-radius-sm);
cursor: pointer;
}
.tasknotes-plugin .pomodoro-stats-view__session-delete-button:hover,
.tasknotes-plugin .pomodoro-stats-view__session-delete-button:focus-visible {
color: var(--tn-color-error);
background: var(--tn-interactive-hover);
border-color: var(--tn-border-color);
}
/* ================================================
RESPONSIVE DESIGN
================================================ */
@ -352,6 +373,10 @@
.tasknotes-plugin .pomodoro-stats-view__session-status {
justify-self: center;
}
.tasknotes-plugin .pomodoro-stats-view__session-delete-button {
justify-self: center;
}
}
@media (max-width: 480px) {

View file

@ -0,0 +1,151 @@
jest.mock("obsidian-daily-notes-interface", () => ({
appHasDailyNotesPluginLoaded: jest.fn(),
createDailyNote: jest.fn(),
getAllDailyNotes: jest.fn(),
getDailyNote: jest.fn(),
}));
import {
appHasDailyNotesPluginLoaded,
getAllDailyNotes,
getDailyNote,
} from "obsidian-daily-notes-interface";
import { PomodoroService } from "../../../src/services/PomodoroService";
import type { PomodoroSessionHistory } from "../../../src/types";
const mockedAppHasDailyNotesPluginLoaded =
appHasDailyNotesPluginLoaded as jest.MockedFunction<typeof appHasDailyNotesPluginLoaded>;
const mockedGetAllDailyNotes = getAllDailyNotes as jest.MockedFunction<typeof getAllDailyNotes>;
const mockedGetDailyNote = getDailyNote as jest.MockedFunction<typeof getDailyNote>;
function createSession(id: string, startTime: string): PomodoroSessionHistory {
const endTime = new Date(new Date(startTime).getTime() + 25 * 60 * 1000).toISOString();
return {
id,
startTime,
endTime,
plannedDuration: 25,
type: "work",
taskPath: `Tasks/${id}.md`,
completed: true,
activePeriods: [{ startTime, endTime }],
};
}
function dateKeyFromLocalDate(date: Date): string {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, "0");
const day = String(date.getDate()).padStart(2, "0");
return `${year}-${month}-${day}`;
}
function createPlugin(options: {
storageLocation: "plugin" | "daily-notes";
pluginHistory?: PomodoroSessionHistory[];
dailyNoteSessions?: Record<string, PomodoroSessionHistory[]>;
}) {
const dailyNoteSessions = options.dailyNoteSessions ?? {};
const dailyNotes = Object.fromEntries(
Object.keys(dailyNoteSessions).map((dateKey) => [
dateKey,
{ path: `Daily/${dateKey}.md` },
])
);
let pluginData = { pomodoroHistory: options.pluginHistory ?? [] };
mockedGetAllDailyNotes.mockReturnValue(dailyNotes as any);
mockedGetDailyNote.mockImplementation((momentValue: any, notes: Record<string, any>) => {
const date =
typeof momentValue?.toDate === "function"
? momentValue.toDate()
: momentValue?.date instanceof Date
? momentValue.date
: momentValue;
return notes[dateKeyFromLocalDate(date)];
});
return {
settings: {
pomodoroWorkDuration: 25,
pomodoroStorageLocation: options.storageLocation,
},
i18n: {
translate: jest.fn((key: string) => key),
},
loadData: jest.fn(async () => pluginData),
saveData: jest.fn(async (data: typeof pluginData) => {
pluginData = data;
}),
app: {
fileManager: {
processFrontMatter: jest.fn(
async (file: { path: string }, callback: (frontmatter: any) => void) => {
const dateKey = file.path.match(/(\d{4}-\d{2}-\d{2})/)?.[1] ?? "";
const frontmatter = {
pomodoros: [...(dailyNoteSessions[dateKey] ?? [])],
};
callback(frontmatter);
dailyNoteSessions[dateKey] = frontmatter.pomodoros;
}
),
},
metadataCache: {
getFileCache: jest.fn(),
},
},
fieldMapper: {
toUserField: jest.fn(() => "pomodoros"),
},
emitter: {
trigger: jest.fn(),
},
taskService: {
startTimeTracking: jest.fn(),
stopTimeTracking: jest.fn(),
},
_dailyNoteSessions: dailyNoteSessions,
};
}
describe("Issue #1147: deleting Pomodoro session history", () => {
beforeEach(() => {
mockedAppHasDailyNotesPluginLoaded.mockReturnValue(true);
});
afterEach(() => {
jest.clearAllMocks();
});
it("removes a session from plugin storage", async () => {
const deletedSession = createSession("delete-me", "2026-04-26T09:00:00.000Z");
const keptSession = createSession("keep-me", "2026-04-26T10:00:00.000Z");
const plugin = createPlugin({
storageLocation: "plugin",
pluginHistory: [deletedSession, keptSession],
});
const service = new PomodoroService(plugin as any);
await expect(service.deleteSessionFromHistory(deletedSession)).resolves.toBe(true);
expect(plugin.saveData).toHaveBeenCalledWith({ pomodoroHistory: [keptSession] });
});
it("removes a session from daily-note storage and legacy plugin history", async () => {
const deletedSession = createSession("delete-me", "2026-04-26T09:00:00.000Z");
const keptSession = createSession("keep-me", "2026-04-26T10:00:00.000Z");
const plugin = createPlugin({
storageLocation: "daily-notes",
pluginHistory: [deletedSession],
dailyNoteSessions: {
"2026-04-26": [deletedSession, keptSession],
},
});
const service = new PomodoroService(plugin as any);
await expect(service.deleteSessionFromHistory(deletedSession)).resolves.toBe(true);
expect(plugin.saveData).toHaveBeenCalledWith({ pomodoroHistory: [] });
expect(plugin._dailyNoteSessions["2026-04-26"]).toEqual([keptSession]);
});
});