Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
6134722
edited notes: move edited-notes related code to own module
contributor Nov 10, 2025
fda2fb9
edited notes: recognize dateNote label value TODAY, MONTH, YEAR
contributor Nov 11, 2025
f5ad2ca
edited notes: handle timezone differences between client and server
contributor Nov 11, 2025
9563bdc
edited notes: force tests to run in UTC timezone
contributor Nov 11, 2025
d7f7ced
edited notes: better docstring for resolveDateParams
contributor Nov 12, 2025
cce2bc7
lint format
contributor Nov 12, 2025
152c7ae
edited notes: extract sqlquery var for eslint to recognize indentation
contributor Nov 12, 2025
bcb8f29
edited notes: move formatMap on module level
contributor Nov 12, 2025
9892298
edited notes: extendable EditedNotesResponse
contributor Nov 11, 2025
dd9f791
edited notes: return limit in response
contributor Nov 16, 2025
452b838
edited notes: add happy path tests
contributor Nov 16, 2025
e18fc4c
edited notes: more descriptive name dateNoteLabelKeywordToDateFilter
contributor Nov 16, 2025
366a8bd
edited notes: better names in tests
contributor Nov 16, 2025
e1170b2
edited notes: recognize not valid dates/keywords, return 0 limit to c…
contributor Nov 16, 2025
0ee7a4e
edited notes: add positive delta test
contributor Nov 16, 2025
00770dc
edited notes: more restrictive check
contributor Nov 16, 2025
d3c0e8b
edited notes: do not shadow variable
contributor Nov 16, 2025
f083908
edited notes: use parameterized limit
contributor Nov 16, 2025
2a65b85
edited notes: allow spaces between keyword and delta in #dateNote
contributor Nov 17, 2025
0cec3c7
make EditedNotes standalone component independent from ribbon
contributor Nov 21, 2025
4006743
rename conflicting type name
contributor Nov 21, 2025
01f571e
edited notes: handle promise rejection
contributor Nov 21, 2025
fb2dd0b
edited notes: better reuse of EditedNotes component with showNotePath…
contributor Nov 22, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions apps/client/src/widgets/EditedNotes.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { useEffect, useState } from "preact/hooks";
import { EditedNotesResponse, EditedNote } from "@triliumnext/commons";
import server from "../services/server";
import { t } from "../services/i18n";
import froca from "../services/froca";
import NoteLink from "./react/NoteLink";
import { joinElements } from "./react/react_utils";

interface EditedNotesProps {
noteId?: string,
dateFilter: string,
showNotePath?: boolean,
}

export default function EditedNotes({ noteId, dateFilter, showNotePath = true } : EditedNotesProps) {
const [ editedNotes, setEditedNotes ] = useState<EditedNote[]>();

useEffect(() => {
if (!noteId || !dateFilter) return;
server.get<EditedNotesResponse>(`edited-notes/${dateFilter}`)
.then(async response => {
const filteredNotes = response.notes.filter((n) => n.noteId !== noteId);
const noteIds = filteredNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(filteredNotes);
})
.catch(err => {
console.error("Failed to fetch edited notes:", err);
setEditedNotes([]);
});
}, [noteId, dateFilter]);

return (
<>
{editedNotes?.length ? (
<div className="edited-notes-list use-tn-links">
{joinElements(editedNotes.map(editedNote => {
return (
<span className="edited-note-line">
{editedNote.isDeleted ? (
<i>{`${editedNote.title} ${t("edited_notes.deleted")}`}</i>
) : (
<>
{editedNote.notePath ? <NoteLink notePath={editedNote.notePath} showNotePath={showNotePath} /> : <span>{editedNote.title}</span>}
</>
)}
</span>
)
}), " ")}
</div>
) : (
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
)}
</>
)
}
40 changes: 3 additions & 37 deletions apps/client/src/widgets/ribbon/EditedNotesTab.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,8 @@
import { useEffect, useState } from "preact/hooks";
import { TabContext } from "./ribbon-interface";
import { EditedNotesResponse } from "@triliumnext/commons";
import server from "../../services/server";
import { t } from "../../services/i18n";
import froca from "../../services/froca";
import NoteLink from "../react/NoteLink";
import { joinElements } from "../react/react_utils";
import EditedNotes from "../../widgets/EditedNotes"

export default function EditedNotesTab({ note }: TabContext) {
const [ editedNotes, setEditedNotes ] = useState<EditedNotesResponse>();

useEffect(() => {
if (!note) return;
server.get<EditedNotesResponse>(`edited-notes/${note.getLabelValue("dateNote")}`).then(async editedNotes => {
editedNotes = editedNotes.filter((n) => n.noteId !== note.noteId);
const noteIds = editedNotes.flatMap((n) => n.noteId);
await froca.getNotes(noteIds, true); // preload all at once
setEditedNotes(editedNotes);
});
}, [ note?.noteId ]);
const dateNoteLabelValue = note?.getLabelValue("dateNote") || "";

return (
<div className="edited-notes-widget" style={{
Expand All @@ -27,25 +11,7 @@ export default function EditedNotesTab({ note }: TabContext) {
width: "100%",
overflow: "auto"
}}>
{editedNotes?.length ? (
<div className="edited-notes-list use-tn-links">
{joinElements(editedNotes.map(editedNote => {
return (
<span className="edited-note-line">
{editedNote.isDeleted ? (
<i>{`${editedNote.title} ${t("edited_notes.deleted")}`}</i>
) : (
<>
{editedNote.notePath ? <NoteLink notePath={editedNote.notePath} showNotePath /> : <span>{editedNote.title}</span> }
</>
)}
</span>
)
}), " ")}
</div>
) : (
<div className="no-edited-notes-found">{t("edited_notes.no_edited_notes_found")}</div>
)}
<EditedNotes noteId={note?.noteId} dateFilter={dateNoteLabelValue} />
</div>
)
}
124 changes: 124 additions & 0 deletions apps/server/src/routes/api/edited-notes.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
import cls from '../../services/cls.js';
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
import { dateNoteLabelKeywordToDateFilter } from "./edited-notes.js";

// test date setup
// client: UTC+1
// server: UTC
// day/month/year is changed when server converts a client date to to UTC
const clientDate = "2025-01-01 00:11:11.000+0100";
const serverDate = "2024-12-31 23:11:11.000Z";

// expected values - from client's point of view
const expectedToday = "2025-01-01";
const expectedTodayMinus1 = "2024-12-31";
const expectedTodayPlus1 = "2025-01-02";
const expectedMonth = "2025-01";
const expectedMonthMinus2 = "2024-11";
const expectedYear = "2025";
const expectedYearMinus1 = "2024";

function keywordResolvesToDate(dateStrOrKeyword: string, expectedDate: string) {
cls.init(() => {
cls.set("localNowDateTime", clientDate);
const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrOrKeyword);
expect(dateFilter.date).toBe(expectedDate);
});
}

function keywordDoesNotResolve(dateStrOrKeyword: string) {
cls.init(() => {
cls.set("localNowDateTime", clientDate);
const dateFilter = dateNoteLabelKeywordToDateFilter(dateStrOrKeyword);
expect(dateFilter.date).toBe(null);
});
}

describe("edited-notes::dateNoteLabelKeywordToDateFilter", () => {
beforeEach(() => {
vi.stubEnv("TZ", "UTC");
vi.useFakeTimers();
vi.setSystemTime(new Date(serverDate));
});

afterEach(() => {
vi.unstubAllEnvs();
// Restore real timers after each test
vi.useRealTimers();
});

it("resolves 'TODAY' to today's date", () => {
keywordResolvesToDate("TODAY", expectedToday);
});

it("resolves 'TODAY+1' to tomorrow's date", () => {
keywordResolvesToDate("TODAY+1", expectedTodayPlus1);
});

it("resolves 'MONTH' to current month", () => {
keywordResolvesToDate("MONTH", expectedMonth);
});

it("resolves 'YEAR' to current year", () => {
keywordResolvesToDate("YEAR", expectedYear);
});

it("resolves 'TODAY-1' to yesterday's date", () => {
keywordResolvesToDate("TODAY-1", expectedTodayMinus1);
});

it("resolves 'TODAY - 1' (with spaces) to yesterday's date", () => {
keywordResolvesToDate("TODAY - 1", expectedTodayMinus1);
keywordResolvesToDate("TODAY- 1", expectedTodayMinus1);
keywordResolvesToDate("TODAY -1", expectedTodayMinus1);
});

it("resolves 'MONTH-2' to 2 months ago", () => {
keywordResolvesToDate("MONTH-2", expectedMonthMinus2);
});

it("resolves 'YEAR-1' to last year", () => {
keywordResolvesToDate("YEAR-1", expectedYearMinus1);
});

it("returns original string for day", () => {
keywordResolvesToDate("2020-12-31", "2020-12-31");
});

it("returns original string for month", () => {
keywordResolvesToDate("2020-12", "2020-12");
});

it("returns original string for partial month", () => {
keywordResolvesToDate("2020-1", "2020-1");
});

it("returns original string for partial month with trailing dash", () => {
keywordResolvesToDate("2020-", "2020-");
});

it("returns original string for year", () => {
keywordResolvesToDate("2020", "2020");
});

it("returns original string for potentially partial day", () => {
keywordResolvesToDate("2020-12-1", "2020-12-1");
});

it("returns null for partial year", () => {
keywordDoesNotResolve("202");
});

it("returns null for arbitrary string", () => {
keywordDoesNotResolve("FOO");
});

it("returns null for missing delta", () => {
keywordDoesNotResolve("TODAY-");
});

it("resolves 'today' (lowercase) to today's date", () => {
keywordResolvesToDate("today", expectedToday);
});

});
Loading