From 3cc02793c826f8e2861ee569333b03e417521dd4 Mon Sep 17 00:00:00 2001 From: Ryan Sandoval Date: Sun, 18 Feb 2024 01:24:39 -0800 Subject: [PATCH 1/7] Add load_test_notes to gitignore --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 301e675..2d77d30 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ dist/ node_modules/ publish/ -coverage/ \ No newline at end of file +coverage/ +load_test/load_test_notes \ No newline at end of file From 983dbfede6288b3c5985939483907f671025fa4a Mon Sep 17 00:00:00 2001 From: Ryan Sandoval Date: Sun, 18 Feb 2024 01:38:14 -0800 Subject: [PATCH 2/7] Add load test scripts --- load_test/load_test.py | 58 ++++++++++++++++++++++++++++++++++++++++++ load_test/template.txt | 7 +++++ 2 files changed, 65 insertions(+) create mode 100644 load_test/load_test.py create mode 100644 load_test/template.txt diff --git a/load_test/load_test.py b/load_test/load_test.py new file mode 100644 index 0000000..5b073c2 --- /dev/null +++ b/load_test/load_test.py @@ -0,0 +1,58 @@ +import os +import random +import shutil +import time +from datetime import datetime + +LOAD_TEST_NOTE_DIR = "load_test_notes" +SECONDS_IN_SIX_MONTHS = 15778800 + +def get_lorem_phrases(): + return [ + "Lorem ipsum dolor sit amet, consectetur adipiscing elit.", + "Sed ut perspiciatis unde omnis iste natus error sit voluptatem.", + "Nemo enim ipsam voluptatem quia voluptas sit aspernatur.", + "Ut enim ad minima veniam, quis nostrum exercitationem ullam.", + "Duis aute irure dolor in reprehenderit in voluptate velit.", + ] + +def generate_files(): + working_dir = os.getcwd() + + if (os.path.isdir(f"{working_dir}/{LOAD_TEST_NOTE_DIR}")): + user_verify = input(f"Directory {LOAD_TEST_NOTE_DIR} already exists. Do you want to delete it? (y/n) ") + if (user_verify == "y"): + shutil.rmtree(f"{working_dir}/{LOAD_TEST_NOTE_DIR}") + else: + print("Cannot generate notes.") + exit(1) + + os.mkdir(f"{working_dir}/{LOAD_TEST_NOTE_DIR}") + + number_of_notes_to_generate = input("How many notes do you want to generate? ") + + if not number_of_notes_to_generate.isnumeric(): + print("Invalid input. Please enter a number.") + exit(1) + + templateString = "" + with open(f"{working_dir}/template.txt", "r") as template: + templateString = template.read() + + for i in range(0, int(number_of_notes_to_generate)): + with open(f"{working_dir}/{LOAD_TEST_NOTE_DIR}/{i}.md", "w") as f: + created_time = time.time() + random.randint(-SECONDS_IN_SIX_MONTHS, SECONDS_IN_SIX_MONTHS) + updated_time = created_time + random.randint(-SECONDS_IN_SIX_MONTHS, SECONDS_IN_SIX_MONTHS) + noteString = templateString.replace("{title}", f"Note {i}") \ + .replace("{created}", f"{datetime.fromtimestamp(created_time).isoformat()}") \ + .replace("{updated}", f"{datetime.fromtimestamp(updated_time).isoformat()}") \ + + noteString += random.choice(get_lorem_phrases()) + + f.write(noteString) + + print(f"Successfully generated {number_of_notes_to_generate} notes.") + + +if __name__ == "__main__": + generate_files() \ No newline at end of file diff --git a/load_test/template.txt b/load_test/template.txt new file mode 100644 index 0000000..32f1909 --- /dev/null +++ b/load_test/template.txt @@ -0,0 +1,7 @@ +--- +title: {title} +updated: {updated} +created: {created} +--- + + From bab92de7cf8982e79bdb734a42af7a7c36aceba2 Mon Sep 17 00:00:00 2001 From: Ryan Sandoval Date: Sun, 18 Feb 2024 00:24:04 -0800 Subject: [PATCH 3/7] Fix getNearestPastDayWithNote not accounting for NoteTypes --- src/gui/index.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/gui/index.tsx b/src/gui/index.tsx index d26f986..d33a8ee 100644 --- a/src/gui/index.tsx +++ b/src/gui/index.tsx @@ -46,6 +46,7 @@ async function getNearestPastDayWithNote( type: MsgType.GetNearestDayWithNote, date: selectedDate.toISOString(), direction: "past", + noteSearchTypes, } as GetNearestDayWithNoteRequest); if (!response) { @@ -117,7 +118,7 @@ function App() { setSelectedDate(selectedDate.clone().add(1, "week")); } }, - [selectedDate, setSelectedDate] + [selectedDate, setSelectedDate, noteSearchTypes] ); return ( From 988347b473d8e4c6ed75e72f079768bf005f4fac Mon Sep 17 00:00:00 2001 From: Ryan Sandoval Date: Sun, 18 Feb 2024 17:07:25 -0800 Subject: [PATCH 4/7] Fix infinite pagination when fetching day notes. Related to: https://github.com/rsandz/joplin-calendar/issues/18 --- src/handlers/GetNotesForDay.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/handlers/GetNotesForDay.ts b/src/handlers/GetNotesForDay.ts index 25b6c88..0109598 100644 --- a/src/handlers/GetNotesForDay.ts +++ b/src/handlers/GetNotesForDay.ts @@ -27,6 +27,7 @@ async function getNotesForDay(date: moment.Moment, operatorTerm: string) { page: page, }); notes.push(...paginatedResponse.items); + page++; } while (paginatedResponse.has_more); return notes From 07025f8b279b1f13192f8e6af02fdcf7fde2cd52 Mon Sep 17 00:00:00 2001 From: Ryan Sandoval Date: Sun, 18 Feb 2024 17:41:53 -0800 Subject: [PATCH 5/7] Fix calendar toggle error when setting is changed --- src/index.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index 08ca1e6..8bb88bc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,11 +25,17 @@ joplin.plugins.register({ await onSettingChange(SHOW_CALENDAR_BUTTON, async (value) => { if (value) { - await joplin.views.toolbarButtons.create( - "calendarToggleButton", - TOGGLE_CALENDAR_COMMAND, - ToolbarButtonLocation.NoteToolbar - ); + try { + await joplin.views.toolbarButtons.create( + "calendarToggleButton", + TOGGLE_CALENDAR_COMMAND, + ToolbarButtonLocation.NoteToolbar + ); + } catch (e) { + // Joplin doesn't allow checking if a button already exists. + // This means button already exists and we don't need to do anything. + return; + } } }); From 4435f2047beb77ec91774397e179f55f1a497944 Mon Sep 17 00:00:00 2001 From: Ryan Sandoval Date: Sun, 18 Feb 2024 00:26:23 -0800 Subject: [PATCH 6/7] Implement Related Notes --- src/constants/NoteSearchTypes.ts | 1 + src/constants/Settings.ts | 1 + src/gui/NoteList/NoteList.tsx | 35 +++++- src/gui/NoteList/__tests__/NoteList.test.tsx | 8 +- src/gui/hooks/useNoteSearchTypes.ts | 12 +- src/handlers/GetMonthStatistics.ts | 7 ++ src/handlers/GetNearestDayWithNote.ts | 56 +++++++++ src/handlers/GetNotesForDay.ts | 28 +++++ src/handlers/GlobalSettings.ts | 12 ++ src/handlers/PanelMessageHandler.ts | 60 ++++++++-- .../__test__/GetNearestDayWithNote.test.ts | 111 +++++++++++++++++- src/handlers/__test__/GetNotesForDay.test.ts | 46 +++++++- src/settings.ts | 14 +++ 13 files changed, 370 insertions(+), 21 deletions(-) create mode 100644 src/handlers/GlobalSettings.ts diff --git a/src/constants/NoteSearchTypes.ts b/src/constants/NoteSearchTypes.ts index bc3e962..747ebf2 100644 --- a/src/constants/NoteSearchTypes.ts +++ b/src/constants/NoteSearchTypes.ts @@ -4,6 +4,7 @@ enum NoteSearchTypes { Created, Modified, + Related, } export default NoteSearchTypes; diff --git a/src/constants/Settings.ts b/src/constants/Settings.ts index e8899a8..c37d63e 100644 --- a/src/constants/Settings.ts +++ b/src/constants/Settings.ts @@ -1,5 +1,6 @@ export const SHOW_CALENDAR_BUTTON = "showCalendarToggleOnToolbar"; export const SHOW_MODIFIED_NOTES = "showModifiedNotes"; +export const SHOW_RELATED_NOTES = "showRelatedNotes"; export const WEEK_START_DAY = "weekStartDay"; export enum WeekStartDay { diff --git a/src/gui/NoteList/NoteList.tsx b/src/gui/NoteList/NoteList.tsx index 1db186d..06e448c 100644 --- a/src/gui/NoteList/NoteList.tsx +++ b/src/gui/NoteList/NoteList.tsx @@ -84,6 +84,7 @@ function NoteList(props: NoteListProps) { if (message.type === MsgType.NoteChanged) { refetchCreatedNotes(); refetchModifiedNotes(); + refetchRelatedNotes(); refetchSelectedNote(); } }); @@ -118,6 +119,21 @@ function NoteList(props: NoteListProps) { enabled: noteSearchTypes.includes(NoteSearchTypes.Modified), }); + const { data: relatedNotesData, refetch: refetchRelatedNotes } = useQuery< + Note[] + >({ + queryKey: ["notes", "related", currentDate.toISOString()], + queryFn: async () => { + console.debug(`Requesting notes for ${currentDate.toLocaleString()}`); + return await webviewApi.postMessage({ + type: MsgType.GetNotes, + currentDate: currentDate.toISOString(), + noteSearchTypes: [NoteSearchTypes.Related], + }); + }, + enabled: noteSearchTypes.includes(NoteSearchTypes.Related), + }); + const { data: selectedNote, refetch: refetchSelectedNote } = useQuery({ queryKey: ["selectedNote"], queryFn: async () => { @@ -158,13 +174,28 @@ function NoteList(props: NoteListProps) { /> + {noteSearchTypes.includes(NoteSearchTypes.Related) && ( + <> + Related Notes + + `${moment(note.createdTime).format("LT")}` + } + />{" "} + + )} Created Notes `${moment(note.createdTime).format("LT")}` } @@ -177,7 +208,7 @@ function NoteList(props: NoteListProps) { selectedNoteId={selectedNote?.id} sortBy={sortBy} sortDirection={sortDirection} - key={`ModifiedNotes:currentDate.toISOString()`} + key={`ModifiedNotes:{currentDate.toISOString()}`} primaryTextStrategy={(note) => `${moment(note.updatedTime).format("LT")}` } diff --git a/src/gui/NoteList/__tests__/NoteList.test.tsx b/src/gui/NoteList/__tests__/NoteList.test.tsx index 19b9c10..b24a140 100644 --- a/src/gui/NoteList/__tests__/NoteList.test.tsx +++ b/src/gui/NoteList/__tests__/NoteList.test.tsx @@ -70,7 +70,7 @@ describe("NoteList", () => { postMessageMock.mockReturnValue([]); render(wrapper()); await waitFor(() => - expect(screen.getByText("No Notes Found")).toBeDefined() + expect(screen.getAllByText("No Notes Found")).toHaveLength(1) ); }); @@ -109,9 +109,11 @@ describe("NoteList", () => { }, }); }); - await waitFor(() => expect(screen.getByText("Test Title")).toBeDefined()); + await waitFor(() => + expect(screen.getAllByText("Test Title")).toHaveLength(1) + ); - expect(screen.getByText("Test Title 2").parentElement).toHaveStyle( + expect(screen.getAllByText("Test Title 2")[0].parentElement).toHaveStyle( "background-color: var(--joplin-background-color-hover3);" ); }); diff --git a/src/gui/hooks/useNoteSearchTypes.ts b/src/gui/hooks/useNoteSearchTypes.ts index 3274935..a85ae2b 100644 --- a/src/gui/hooks/useNoteSearchTypes.ts +++ b/src/gui/hooks/useNoteSearchTypes.ts @@ -2,7 +2,7 @@ import NoteSearchTypes from "@constants/NoteSearchTypes"; import MsgType from "@constants/messageTypes"; import { useEffect, useState } from "react"; import useWebviewApiOnMessage from "./useWebViewApiOnMessage"; -import { SHOW_MODIFIED_NOTES } from "@constants/Settings"; +import { SHOW_MODIFIED_NOTES, SHOW_RELATED_NOTES } from "@constants/Settings"; import useOnSettingsChange from "./useOnSettingsChange"; /** @@ -15,10 +15,20 @@ function useNoteSearchTypes() { SHOW_MODIFIED_NOTES, false ); + const showRelatedNotes = useOnSettingsChange( + SHOW_RELATED_NOTES, + false + ); + + // Default note types to show const noteSearchTypes = [NoteSearchTypes.Created]; + if (showModifiedNotes) { noteSearchTypes.push(NoteSearchTypes.Modified); } + if (showRelatedNotes) { + noteSearchTypes.push(NoteSearchTypes.Related); + } return noteSearchTypes; } diff --git a/src/handlers/GetMonthStatistics.ts b/src/handlers/GetMonthStatistics.ts index 98b62d6..3ef459b 100644 --- a/src/handlers/GetMonthStatistics.ts +++ b/src/handlers/GetMonthStatistics.ts @@ -3,6 +3,7 @@ import MonthStatistics from "@constants/MonthStatistics"; import { getCreatedNotesForDay, getModifiedNotesForDay, + getRelatedNotesForDay, } from "./GetNotesForDay"; import Note from "@constants/Note"; @@ -61,3 +62,9 @@ export async function getMonthModifiedNoteStatistics( ): Promise { return getMonthStatistics(date, getModifiedNotesForDay); } + +export async function getMonthRelatedNoteStatistics( + date: moment.Moment +): Promise { + return getMonthStatistics(date, getRelatedNotesForDay); +} diff --git a/src/handlers/GetNearestDayWithNote.ts b/src/handlers/GetNearestDayWithNote.ts index 446378a..bd90cc4 100644 --- a/src/handlers/GetNearestDayWithNote.ts +++ b/src/handlers/GetNearestDayWithNote.ts @@ -6,6 +6,7 @@ import { convertSnakeCaseKeysToCamelCase, convertEpochDateInNoteToIsoString, } from "./Transforms"; +import { getDateFormat } from "./GlobalSettings"; /** * Get notes in past or future matching the specific operator term. @@ -77,3 +78,58 @@ export async function getNearestDayWithModifiedNote( "user_updated_time" ); } + +const RELATED_NOTE_MAX_DAYS_TO_SEARCH = 120; // ~4 months + +/** + * Gets nearest day with related notes. + * Related notes are notes that have the day in the title. + * + * @param startDate The date where search will begin from + * @param direction "future" or "past" + * + * @param earlyStopDate If previous notes from other criteria are found, provide + * the date to stop searching at. Increases responsiveness of search. + */ +export async function getNearestDayWithRelatedNote( + startDate: moment.Moment, + direction: "future" | "past", + earlyStopDate: moment.Moment | null +): Promise { + const dateFormat = await getDateFormat(); + + const workingDate = startDate.clone(); + for (let i = 0; i < RELATED_NOTE_MAX_DAYS_TO_SEARCH; i++) { + // Don't include the startDate + if (direction === "past") { + workingDate.subtract(1, "day"); + } else { + workingDate.add(1, "day"); + } + + if (earlyStopDate && workingDate.isSame(earlyStopDate, "day")) { + return null; + } + + const dateString = workingDate.format(dateFormat); + + const response = await joplin.data.get(["search"], { + fields: ["id", "title", "user_created_time", "user_updated_time"], + limit: 1, + query: `title:/"${dateString}"`, + }); + + if (response.items.length > 0) { + let note = response.items[0]; + note = removeUserTermFromUserTimes(note); + note = convertSnakeCaseKeysToCamelCase(note); + const transformedNote = convertEpochDateInNoteToIsoString(note); + return { + note: transformedNote, + date: workingDate.toISOString(), + }; + } + } + + return null; +} diff --git a/src/handlers/GetNotesForDay.ts b/src/handlers/GetNotesForDay.ts index 0109598..48d2a35 100644 --- a/src/handlers/GetNotesForDay.ts +++ b/src/handlers/GetNotesForDay.ts @@ -4,6 +4,7 @@ import { convertSnakeCaseKeysToCamelCase, convertEpochDateInNoteToIsoString, } from "./Transforms"; +import { getDateFormat } from "./GlobalSettings"; /** * Get notes for a specific day using the provided operator term. @@ -43,3 +44,30 @@ export async function getCreatedNotesForDay(date: moment.Moment) { export async function getModifiedNotesForDay(date: moment.Moment) { return getNotesForDay(date, "updated"); } + +/** + * Gets list of related notes for a specific day. + * Related notes are notes that have the day in the title. + */ +export async function getRelatedNotesForDay(date: moment.Moment) { + const notes = []; + + const dateString = date.format(await getDateFormat()); + + let page = 1; + let paginatedResponse: Record; + do { + paginatedResponse = await joplin.data.get(["search"], { + fields: ["id", "title", "user_created_time", "user_updated_time"], + query: `title:/"${dateString}"`, + page: page, + }); + notes.push(...paginatedResponse.items); + page++; + } while (paginatedResponse.has_more); + + return notes + .map(removeUserTermFromUserTimes) + .map(convertSnakeCaseKeysToCamelCase) + .map(convertEpochDateInNoteToIsoString); +} diff --git a/src/handlers/GlobalSettings.ts b/src/handlers/GlobalSettings.ts new file mode 100644 index 0000000..5ba8dda --- /dev/null +++ b/src/handlers/GlobalSettings.ts @@ -0,0 +1,12 @@ +import joplin from "api"; + +const DATE_FORMAT_KEY = "dateFormat"; + +/** + * Get's the user's date format. + * + * @see https://github.com/laurent22/joplin/blob/dev/packages/lib/models/Setting.ts#L799 + */ +export async function getDateFormat() { + return (await joplin.settings.globalValue(DATE_FORMAT_KEY)) as string; +} diff --git a/src/handlers/PanelMessageHandler.ts b/src/handlers/PanelMessageHandler.ts index 11f32ce..910e634 100644 --- a/src/handlers/PanelMessageHandler.ts +++ b/src/handlers/PanelMessageHandler.ts @@ -4,14 +4,17 @@ import moment from "moment"; import { getNearestDayWithCreatedNote, getNearestDayWithModifiedNote, + getNearestDayWithRelatedNote, } from "./GetNearestDayWithNote"; import { getMonthCreatedNoteStatistics, getMonthModifiedNoteStatistics, + getMonthRelatedNoteStatistics, } from "./GetMonthStatistics"; import { getCreatedNotesForDay, getModifiedNotesForDay, + getRelatedNotesForDay, } from "./GetNotesForDay"; import NoteSearchTypes from "@constants/NoteSearchTypes"; import MonthStatistics from "@constants/MonthStatistics"; @@ -48,6 +51,13 @@ async function handleGetNotes(message) { )) ); } + if (noteTypes.includes(NoteSearchTypes.Related)) { + notes.push( + ...(await getRelatedNotesForDay( + moment(message.currentDate, moment.ISO_8601) + )) + ); + } const uniqueNotes = uniqBy(notes, (note) => note.id); return uniqueNotes; @@ -79,6 +89,12 @@ async function handleGetMonthStatistics(message): Promise { ); } + if (noteTypes.includes(NoteSearchTypes.Related)) { + individualStatistics.push( + await getMonthRelatedNoteStatistics(moment(message.date, moment.ISO_8601)) + ); + } + return individualStatistics.reduce( (prev, curr) => { const notesPerDay = prev.notesPerDay; @@ -107,6 +123,25 @@ async function handleGetNearestDayWithNote(message) { return null; } + const candidateDateReducer = (responses) => + responses.reduce((prev, curr) => { + if (!curr) { + return prev; + } + if (!prev) { + return curr; + } + + const currDate = moment(curr.date, moment.ISO_8601); + const prevDate = moment(prev.date, moment.ISO_8601); + + if (message.direction === "future") { + return currDate.isBefore(prevDate) ? curr : prev; + } else { + return currDate.isAfter(prevDate) ? curr : prev; + } + }, null); + const candidateResponses: GetNearestDayWithNoteResponse[] = []; if (noteTypes.includes(NoteSearchTypes.Created)) { @@ -126,19 +161,18 @@ async function handleGetNearestDayWithNote(message) { ); } - return candidateResponses.reduce((prev, curr) => { - if (!curr) { - return prev; - } - if (!prev) { - return curr; - } - if (message.direction === "future") { - return curr.date < prev.date ? curr : prev; - } else { - return curr.date > prev.date ? curr : prev; - } - }, null); + if (noteTypes.includes(NoteSearchTypes.Related)) { + const earlyStopDate = candidateDateReducer(candidateResponses); + candidateResponses.push( + await getNearestDayWithRelatedNote( + moment(message.date, moment.ISO_8601), + message.direction, + earlyStopDate + ) + ); + } + + return candidateDateReducer(candidateResponses); } /** diff --git a/src/handlers/__test__/GetNearestDayWithNote.test.ts b/src/handlers/__test__/GetNearestDayWithNote.test.ts index a3b93e7..b96f79c 100644 --- a/src/handlers/__test__/GetNearestDayWithNote.test.ts +++ b/src/handlers/__test__/GetNearestDayWithNote.test.ts @@ -3,7 +3,12 @@ import handlePanelMessage from "../PanelMessageHandler"; import MsgType from "@constants/messageTypes"; import joplin from "api"; import moment from "moment"; -import { getNearestDayWithModifiedNote } from "../GetNearestDayWithNote"; +import { + getNearestDayWithModifiedNote, + getNearestDayWithRelatedNote, +} from "../GetNearestDayWithNote"; +import { getDateFormat } from "../GlobalSettings"; +import _ from "lodash"; jest.mock( "api", @@ -16,6 +21,9 @@ jest.mock( ); const mockedJoplin = jest.mocked(joplin); +jest.mock("../GlobalSettings"); +const mockedGetDateFormat = jest.mocked(getDateFormat); + describe("GetNearestDayWithNote", () => { const baselineDate = moment("1970-01-01", "YYYY-MM-DD"); @@ -137,3 +145,104 @@ describe("GetNearestDayWithNote", () => { expect(result).toBeNull(); }); }); + +describe("Get Nearest Related Note", () => { + mockedGetDateFormat.mockResolvedValue("YYYY/MM/DD"); + + const apiReturnNote = Object.freeze({ + id: "testId", + title: "testTitle", + user_created_time: moment.utc("2024-02-17", "YYYY-MM-DD").valueOf(), + user_updated_time: moment.utc("2024-02-17", "YYYY-MM-DD").valueOf(), + }); + + const transformedNote = Object.freeze({ + id: "testId", + title: "testTitle", + createdTime: "2024-02-17T00:00:00.000Z", + updatedTime: "2024-02-17T00:00:00.000Z", + }); + + it("returns a related note in future if found", async () => { + mockedJoplin.data.get.mockImplementation((path, query) => { + if (query.query === 'title:/"2024/02/17"') { + return Promise.resolve({ + items: [_.clone(apiReturnNote)], + }); + } else { + return Promise.resolve({ + items: [], + }); + } + }); + + const result = await getNearestDayWithRelatedNote( + moment.utc("2024-02-14", "YYYY-MM-DD"), + "future", + null + ); + + expect(result).toStrictEqual({ + date: moment.utc("2024-02-17", "YYYY-MM-DD").toISOString(), + note: transformedNote, + }); + }); + + it("returns a related note in past if found", async () => { + mockedJoplin.data.get.mockImplementation((path, query) => { + if (query.query === 'title:/"2024/02/17"') { + return Promise.resolve({ + items: [_.cloneDeep(apiReturnNote)], + }); + } else { + return Promise.resolve({ + items: [], + }); + } + }); + + const result = await getNearestDayWithRelatedNote( + moment.utc("2024-02-24", "YYYY-MM-DD"), + "past", + null + ); + + expect(result).toStrictEqual({ + date: moment.utc("2024-02-17", "YYYY-MM-DD").toISOString(), + note: transformedNote, + }); + }); + + it("stops early if given a stopEarly date", async () => { + mockedJoplin.data.get.mockImplementation((path, query) => { + if (query.query === 'title:"2024/02/17"') { + return Promise.resolve({ + items: [apiReturnNote], + }); + } else { + return Promise.resolve({ + items: [], + }); + } + }); + + const result = await getNearestDayWithRelatedNote( + moment.utc("2024-02-14", "YYYY-MM-DD"), + "future", + moment.utc("2024-02-15", "YYYY-MM-DD") + ); + + expect(result).toStrictEqual(null); + }); + + it("returns null if no notes found", async () => { + mockedJoplin.data.get.mockResolvedValue({ + items: [], + }); + const result = await getNearestDayWithRelatedNote( + moment("1970-01-01", "YYYY-MM-DD"), + "past", + null + ); + }); +}); diff --git a/src/handlers/__test__/GetNotesForDay.test.ts b/src/handlers/__test__/GetNotesForDay.test.ts index 07b1989..5e74e53 100644 --- a/src/handlers/__test__/GetNotesForDay.test.ts +++ b/src/handlers/__test__/GetNotesForDay.test.ts @@ -1,6 +1,10 @@ import joplin from "api"; import moment from "moment"; -import { getCreatedNotesForDay } from "../GetNotesForDay"; +import { + getCreatedNotesForDay, + getRelatedNotesForDay, +} from "../GetNotesForDay"; +import { getDateFormat } from "../GlobalSettings"; jest.mock( "api", @@ -15,6 +19,9 @@ jest.mock( ); const mockedJoplin = jest.mocked(joplin); +jest.mock("../GlobalSettings"); +const mockedGetDateFormat = jest.mocked(getDateFormat); + describe("Get Notes For Day", () => { it("calls joplin api with the correct query", async () => { getCreatedNotesForDay(moment("2023-05-29")); @@ -91,3 +98,40 @@ describe("Get Notes For Day", () => { }); }); }); + +describe("Get Related Notes for day", () => { + mockedGetDateFormat.mockResolvedValue("YYYY/MM/DD"); + + it("calls joplin api with the correct query", async () => { + await getRelatedNotesForDay(moment("2023-05-29")); + + expect(mockedJoplin.data.get).toHaveBeenCalledWith(["search"], { + fields: expect.any(Array), + query: 'title:/"2023/05/29"', + page: expect.any(Number), + }); + }); + + it("returns single note correctly", async () => { + mockedJoplin.data.get.mockImplementationOnce(async () => ({ + items: [ + { + id: "testId", + title: "2023/05/29", + user_created_time: 0, + user_updated_time: 0, + }, + ], + })); + + const result = await getRelatedNotesForDay(moment("2023-05-29")); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual({ + id: "testId", + title: "2023/05/29", + createdTime: "1970-01-01T00:00:00.000Z", + updatedTime: "1970-01-01T00:00:00.000Z", + }); + }); +}); diff --git a/src/settings.ts b/src/settings.ts index 4340138..f685a93 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -4,9 +4,11 @@ import MsgType from "@constants/messageTypes"; import { SHOW_CALENDAR_BUTTON, SHOW_MODIFIED_NOTES, + SHOW_RELATED_NOTES, WEEK_START_DAY, WeekStartDay, } from "@constants/Settings"; +import { getDateFormat } from "./handlers/GlobalSettings"; const SETTINGS_SECTION_ID = "joplinCalendarSection"; @@ -37,6 +39,17 @@ export async function registerSettings() { value: true, section: SETTINGS_SECTION_ID, }, + [SHOW_RELATED_NOTES]: { + label: "Show Related Notes in Note List", + description: `Show notes that have the date in the title. The Joplin date + format is used for this. Currently, it is set to: ${await getDateFormat()}. Note, + this feature is experimental and may impact performance with large + notebooks.`, + public: true, + type: SettingItemType.Bool, + value: false, + section: SETTINGS_SECTION_ID, + }, [WEEK_START_DAY]: { label: "Week Start Day", description: "Which day the week starts on", @@ -63,6 +76,7 @@ export async function registerSettings() { */ export async function registerPanelAlertOnSettingChange(panelHandle: string) { await onSettingChangeAlertPanel(panelHandle, SHOW_MODIFIED_NOTES); + await onSettingChangeAlertPanel(panelHandle, SHOW_RELATED_NOTES); await onSettingChangeAlertPanel(panelHandle, WEEK_START_DAY); } From 2add08233d83e0201349937fe33e9891267771fe Mon Sep 17 00:00:00 2001 From: Ryan Sandoval Date: Sun, 18 Feb 2024 17:50:52 -0800 Subject: [PATCH 7/7] Update README with Related notes --- README.md | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 51c2ab3..e759203 100644 --- a/README.md +++ b/README.md @@ -9,9 +9,10 @@ This plugin for [Joplin](https://joplinapp.org/) adds a simple calendar which al - The plugin includes a **calendar** and a **notes list**. ## 📆 Calendar + - Clicking on a calendar date shows the notes created or updated on that date. - Clicking on the '<' and '>' buttons above the calendar moves between months. -- Clicking on the '<' and '>' buttons above the calendar *while holding `ctrl`* moves between years. +- Clicking on the '<' and '>' buttons above the calendar _while holding `ctrl`_ moves between years. - Calendar dates have dots beneath them indicating the number of notes written on that day. - Each dot represents 2 notes created, up to a maximum of 4 dots. @@ -19,15 +20,19 @@ This plugin for [Joplin](https://joplinapp.org/) adds a simple calendar which al > The create date and updated date for a note can be manually change by clicking on the "🛈" button. ## 🗒️ Notes List -- The notes list shows notes created and updated on the specified date. - - Showing updated notes can be disabled in the settings. + +- The notes list shows notes created on the specified date. +- The notes list can show notes updated on the specified date. + - This must be enabled in the settings. +- The notes list can show notes related to the specified date. These are notes that have the date in the title of the note. + - This must be enabled in the settings. + - The Joplin date format is used when searching for related notes. - Navigate to the notes by clicking on the titles in the notes list. - Clicking on the '<' and '>' buttons above the notes list moves days. -- Clicking on the '<' and '>' buttons above the notes list *while holding `ctrl`* moves between days with notes. +- Clicking on the '<' and '>' buttons above the notes list _while holding `ctrl`_ moves between days with notes. - Clicking the `today` button brings back the calendar focus to the current day. - Notes can be sorted by time of creation, or alphabetically. The sort direction can also be changed. - ## ⌨️ Keyboard Shortcuts ### Joplin Wide @@ -47,6 +52,11 @@ This plugin for [Joplin](https://joplinapp.org/) adds a simple calendar which al - Use up and down arrow keys to select notes. +## 🛑 Limitations + +- Related notes is still an experimental feature - it will negatively impact performance with larger note books. +- When searching for the nearest note with Related Notes ON, only related notes within the closest 120 days will be checked. + # ⚙️ Development ## Building the plugin