Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Related Notes and Several Fixes #19

Merged
merged 7 commits into from
Feb 19, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
dist/
node_modules/
publish/
coverage/
coverage/
load_test/load_test_notes
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,30 @@ 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.

> [!Tip]
> 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
Expand All @@ -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
Expand Down
58 changes: 58 additions & 0 deletions load_test/load_test.py
Original file line number Diff line number Diff line change
@@ -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()
7 changes: 7 additions & 0 deletions load_test/template.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
title: {title}
updated: {updated}
created: {created}
---


1 change: 1 addition & 0 deletions src/constants/NoteSearchTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
enum NoteSearchTypes {
Created,
Modified,
Related,
}

export default NoteSearchTypes;
1 change: 1 addition & 0 deletions src/constants/Settings.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down
35 changes: 33 additions & 2 deletions src/gui/NoteList/NoteList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ function NoteList(props: NoteListProps) {
if (message.type === MsgType.NoteChanged) {
refetchCreatedNotes();
refetchModifiedNotes();
refetchRelatedNotes();
refetchSelectedNote();
}
});
Expand Down Expand Up @@ -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<Note>({
queryKey: ["selectedNote"],
queryFn: async () => {
Expand Down Expand Up @@ -158,13 +174,28 @@ function NoteList(props: NoteListProps) {
/>
</ButtonBarContainer>
<ListContainer>
{noteSearchTypes.includes(NoteSearchTypes.Related) && (
<>
<NoteTypeHeader>Related Notes</NoteTypeHeader>
<NoteListItems
notes={relatedNotesData ?? []}
selectedNoteId={selectedNote?.id}
sortBy={sortBy}
sortDirection={sortDirection}
key={`RelatedNotes:{currentDate.toISOString()}`}
primaryTextStrategy={(note) =>
`${moment(note.createdTime).format("LT")}`
}
/>{" "}
</>
)}
<NoteTypeHeader>Created Notes</NoteTypeHeader>
<NoteListItems
notes={createdNotesData ?? []}
selectedNoteId={selectedNote?.id}
sortBy={sortBy}
sortDirection={sortDirection}
key={`CreatedNotes:currentDate.toISOString()`}
key={`CreatedNotes:{currentDate.toISOString()}`}
primaryTextStrategy={(note) =>
`${moment(note.createdTime).format("LT")}`
}
Expand All @@ -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")}`
}
Expand Down
8 changes: 5 additions & 3 deletions src/gui/NoteList/__tests__/NoteList.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ describe("NoteList", () => {
postMessageMock.mockReturnValue([]);
render(wrapper(<NoteList currentDate={moment()} />));
await waitFor(() =>
expect(screen.getByText("No Notes Found")).toBeDefined()
expect(screen.getAllByText("No Notes Found")).toHaveLength(1)
);
});

Expand Down Expand Up @@ -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);"
);
});
Expand Down
12 changes: 11 additions & 1 deletion src/gui/hooks/useNoteSearchTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

/**
Expand All @@ -15,10 +15,20 @@ function useNoteSearchTypes() {
SHOW_MODIFIED_NOTES,
false
);
const showRelatedNotes = useOnSettingsChange<boolean>(
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;
}

Expand Down
3 changes: 2 additions & 1 deletion src/gui/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ async function getNearestPastDayWithNote(
type: MsgType.GetNearestDayWithNote,
date: selectedDate.toISOString(),
direction: "past",
noteSearchTypes,
} as GetNearestDayWithNoteRequest);

if (!response) {
Expand Down Expand Up @@ -117,7 +118,7 @@ function App() {
setSelectedDate(selectedDate.clone().add(1, "week"));
}
},
[selectedDate, setSelectedDate]
[selectedDate, setSelectedDate, noteSearchTypes]
);

return (
Expand Down
7 changes: 7 additions & 0 deletions src/handlers/GetMonthStatistics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import MonthStatistics from "@constants/MonthStatistics";
import {
getCreatedNotesForDay,
getModifiedNotesForDay,
getRelatedNotesForDay,
} from "./GetNotesForDay";
import Note from "@constants/Note";

Expand Down Expand Up @@ -61,3 +62,9 @@ export async function getMonthModifiedNoteStatistics(
): Promise<MonthStatistics> {
return getMonthStatistics(date, getModifiedNotesForDay);
}

export async function getMonthRelatedNoteStatistics(
date: moment.Moment
): Promise<MonthStatistics> {
return getMonthStatistics(date, getRelatedNotesForDay);
}
56 changes: 56 additions & 0 deletions src/handlers/GetNearestDayWithNote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
convertSnakeCaseKeysToCamelCase,
convertEpochDateInNoteToIsoString,
} from "./Transforms";
import { getDateFormat } from "./GlobalSettings";

/**
* Get notes in past or future matching the specific operator term.
Expand Down Expand Up @@ -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<GetNearestDayWithNoteResponse | null> {
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;
}