mirror of
https://github.com/callumalpass/tasknotes.git
synced 2026-07-22 12:50:26 +00:00
Add Pomodoro session deletion
This commit is contained in:
parent
cbe2b9acd6
commit
340f629226
6 changed files with 318 additions and 5 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
151
tests/unit/issues/issue-1147-pomodoro-session-delete.test.ts
Normal file
151
tests/unit/issues/issue-1147-pomodoro-session-delete.test.ts
Normal 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]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue