From be2867e1f5906a2050d064dd5434792865e18828 Mon Sep 17 00:00:00 2001 From: Ryan Sandoval <ryan_sandoval@live.com> Date: Sat, 3 Feb 2024 14:10:32 -0800 Subject: [PATCH 1/3] Add setting for week start day --- src/constants/Settings.ts | 6 ++++++ src/index.ts | 6 +++--- src/settings.ts | 25 ++++++++++++++++++++++++- 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/constants/Settings.ts b/src/constants/Settings.ts index 8d1ec3e..e8899a8 100644 --- a/src/constants/Settings.ts +++ b/src/constants/Settings.ts @@ -1,2 +1,8 @@ export const SHOW_CALENDAR_BUTTON = "showCalendarToggleOnToolbar"; export const SHOW_MODIFIED_NOTES = "showModifiedNotes"; +export const WEEK_START_DAY = "weekStartDay"; + +export enum WeekStartDay { + Sunday = "Sunday", + Monday = "Monday", +} diff --git a/src/index.ts b/src/index.ts index 6650e3e..08ca1e6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,9 +7,9 @@ import { registerSettings, triggerAllSettingsCallbacks, onSettingChange, - onSettingChangeAlertPanel, + registerPanelAlertOnSettingChange, } from "./settings"; -import { SHOW_CALENDAR_BUTTON, SHOW_MODIFIED_NOTES } from "@constants/Settings"; +import { SHOW_CALENDAR_BUTTON } from "@constants/Settings"; joplin.plugins.register({ onStart: async function () { @@ -33,7 +33,7 @@ joplin.plugins.register({ } }); - await onSettingChangeAlertPanel(panel, SHOW_MODIFIED_NOTES); + await registerPanelAlertOnSettingChange(panel); await triggerAllSettingsCallbacks(); }, diff --git a/src/settings.ts b/src/settings.ts index 3790730..4340138 100644 --- a/src/settings.ts +++ b/src/settings.ts @@ -1,7 +1,12 @@ import joplin from "api"; import { SettingItemType } from "api/types"; import MsgType from "@constants/messageTypes"; -import { SHOW_CALENDAR_BUTTON, SHOW_MODIFIED_NOTES } from "@constants/Settings"; +import { + SHOW_CALENDAR_BUTTON, + SHOW_MODIFIED_NOTES, + WEEK_START_DAY, + WeekStartDay, +} from "@constants/Settings"; const SETTINGS_SECTION_ID = "joplinCalendarSection"; @@ -32,6 +37,16 @@ export async function registerSettings() { value: true, section: SETTINGS_SECTION_ID, }, + [WEEK_START_DAY]: { + label: "Week Start Day", + description: "Which day the week starts on", + public: true, + isEnum: true, + type: SettingItemType.String, + options: WeekStartDay, + value: WeekStartDay.Sunday, + section: SETTINGS_SECTION_ID, + }, }); await joplin.settings.onChange(async ({ keys }) => { @@ -43,6 +58,14 @@ export async function registerSettings() { }); } +/** + * Alerts panel when certain settings change. + */ +export async function registerPanelAlertOnSettingChange(panelHandle: string) { + await onSettingChangeAlertPanel(panelHandle, SHOW_MODIFIED_NOTES); + await onSettingChangeAlertPanel(panelHandle, WEEK_START_DAY); +} + export async function triggerAllSettingsCallbacks() { Object.entries(settingObservers).forEach(async ([key, callbacks]) => { callbacks.forEach(async (callback) => { From e181be4d036378c5fef634a0081d01694c7c2e2c Mon Sep 17 00:00:00 2001 From: Ryan Sandoval <ryan_sandoval@live.com> Date: Sat, 3 Feb 2024 14:31:34 -0800 Subject: [PATCH 2/3] Implement an onSettingsChange hook --- src/gui/hooks/useNoteSearchTypes.ts | 33 +++---------------- src/gui/hooks/useOnSettingsChange.ts | 48 ++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 28 deletions(-) create mode 100644 src/gui/hooks/useOnSettingsChange.ts diff --git a/src/gui/hooks/useNoteSearchTypes.ts b/src/gui/hooks/useNoteSearchTypes.ts index dcbea79..3274935 100644 --- a/src/gui/hooks/useNoteSearchTypes.ts +++ b/src/gui/hooks/useNoteSearchTypes.ts @@ -3,6 +3,7 @@ import MsgType from "@constants/messageTypes"; import { useEffect, useState } from "react"; import useWebviewApiOnMessage from "./useWebViewApiOnMessage"; import { SHOW_MODIFIED_NOTES } from "@constants/Settings"; +import useOnSettingsChange from "./useOnSettingsChange"; /** * Provides note types to search for when fetching notes, based on user preference. @@ -10,38 +11,14 @@ import { SHOW_MODIFIED_NOTES } from "@constants/Settings"; * Note type examples: Created Notes, Modified Notes */ function useNoteSearchTypes() { - const [showModifiedNotes, setShowModifiedNotes] = useState(false); + const showModifiedNotes = useOnSettingsChange<boolean>( + SHOW_MODIFIED_NOTES, + false + ); const noteSearchTypes = [NoteSearchTypes.Created]; if (showModifiedNotes) { noteSearchTypes.push(NoteSearchTypes.Modified); } - - // Trigger all settings callbacks once initialized - // to prevent any race conditions. - useEffect(() => { - webviewApi.postMessage({ - type: MsgType.TriggerAllSettingsCallbacks, - }); - }, []); - - useWebviewApiOnMessage((data) => { - const message = data.message; - - if (!message.type) { - return; - } - if (message.type !== MsgType.SettingChanged) { - return; - } - - const settingMessage = message as any; - - if (settingMessage.key !== SHOW_MODIFIED_NOTES) { - return; - } - setShowModifiedNotes((message as any).value); - }); - return noteSearchTypes; } diff --git a/src/gui/hooks/useOnSettingsChange.ts b/src/gui/hooks/useOnSettingsChange.ts new file mode 100644 index 0000000..0b36bd1 --- /dev/null +++ b/src/gui/hooks/useOnSettingsChange.ts @@ -0,0 +1,48 @@ +import MsgType from "@constants/messageTypes"; +import { useEffect, useState } from "react"; +import useWebviewApiOnMessage from "./useWebViewApiOnMessage"; + +/** + * Providers the settings value for the given settings key. + * + * @template SettingType The type of the settings value + * + * @param settingKey The settings key + * @returns The settings value + */ +function useOnSettingsChange<SettingType>( + settingKey: string, + defaultValue: SettingType = null +): SettingType { + const [settingValue, setSettingValue] = useState<SettingType>(defaultValue); + + // Trigger all settings callbacks once initialized + // to prevent any race conditions. + useEffect(() => { + webviewApi.postMessage({ + type: MsgType.TriggerAllSettingsCallbacks, + }); + }, []); + + useWebviewApiOnMessage((data) => { + const message = data.message; + + if (!message.type) { + return; + } + if (message.type !== MsgType.SettingChanged) { + return; + } + + const settingMessage = message as any; + + if (settingMessage.key !== settingKey) { + return; + } + setSettingValue((message as any).value); + }); + + return settingValue; +} + +export default useOnSettingsChange; From 01453d7f1b01b0fd9b080c0bb33ae99a13bc6f66 Mon Sep 17 00:00:00 2001 From: Ryan Sandoval <ryan_sandoval@live.com> Date: Sat, 3 Feb 2024 15:40:06 -0800 Subject: [PATCH 3/3] Add support to start week on Monday --- src/gui/Calendar/CalendarHeader.tsx | 13 +++++++++ src/gui/Calendar/__tests__/Calendar.test.tsx | 28 ++++++++++++++++++++ src/gui/Calendar/index.tsx | 27 +++++++++++++++++-- 3 files changed, 66 insertions(+), 2 deletions(-) diff --git a/src/gui/Calendar/CalendarHeader.tsx b/src/gui/Calendar/CalendarHeader.tsx index 5604fa1..b7f966b 100644 --- a/src/gui/Calendar/CalendarHeader.tsx +++ b/src/gui/Calendar/CalendarHeader.tsx @@ -1,6 +1,8 @@ import { weekdaysShort } from "moment"; import React from "react"; import styled from "styled-components"; +import useOnSettingsChange from "../hooks/useOnSettingsChange"; +import { WEEK_START_DAY, WeekStartDay } from "@constants/Settings"; const HeaderCell = styled.th` font-size: var(--joplin-font-size); @@ -19,6 +21,17 @@ function CalendarHeader(props: CalendarHeaderProps) { const calendarHeader = weekdaysShort().map((day) => ( <HeaderCell>{day}</HeaderCell> )); + + const weekStartDay = useOnSettingsChange<WeekStartDay>( + WEEK_START_DAY, + WeekStartDay.Sunday + ); + + // Need to shift the headers based on the week start day + if (weekStartDay === WeekStartDay.Monday) { + calendarHeader.push(calendarHeader.shift()); + } + return <HeaderRow>{...calendarHeader}</HeaderRow>; } diff --git a/src/gui/Calendar/__tests__/Calendar.test.tsx b/src/gui/Calendar/__tests__/Calendar.test.tsx index 39d6db4..e433974 100644 --- a/src/gui/Calendar/__tests__/Calendar.test.tsx +++ b/src/gui/Calendar/__tests__/Calendar.test.tsx @@ -6,10 +6,15 @@ import moment from "moment"; import { act } from "react-dom/test-utils"; import useGetMonthStatistics from "../../hooks/useGetMonthStatistics"; import useWebviewApiOnMessage from "../../hooks/useWebViewApiOnMessage"; +import { WeekStartDay } from "@constants/Settings"; +import useOnSettingsChange from "../../hooks/useOnSettingsChange"; jest.mock("../../hooks/useGetMonthStatistics"); const mockedUseGetMonthStatistics = jest.mocked(useGetMonthStatistics); +jest.mock("../../hooks/useOnSettingsChange"); +const mockedUseOnSettingsChange = jest.mocked(useOnSettingsChange); + global.webviewApi = { postMessage: jest.fn(), onMessage: jest.fn(), @@ -26,6 +31,7 @@ describe("calendar", () => { }, refetch: jest.fn(), }); + mockedUseOnSettingsChange.mockReset(); }); it("displays dates correctly", () => { @@ -51,6 +57,28 @@ describe("calendar", () => { } }); + it("displays dates correctly if week starts on Monday", () => { + mockedUseOnSettingsChange.mockReturnValue(WeekStartDay.Monday); + + const date = moment("May-29-2023", "MMM-DD-YYYY"); + render(<Calendar selectedDate={date} />); + + expect(screen.getByText("May 2023")).toBeDefined(); + + const cells = screen.getAllByRole("cell"); + expect(cells).toHaveLength(42); // 7 days * 6 rows + + // Assert May (No April) + for (let i = 0; i <= 30; i++) { + expect(cells[i].textContent).toEqual((i + 1).toString()); + } + + // Assert June + for (let i = 1; i <= 11; i++) { + expect(cells[i + 30].textContent).toEqual(i.toString()); + } + }); + it("calls callback when next month clicked", () => { const nextMonthCallback = jest.fn(); const date = moment("May-29-2023", "MMM-DD-YYYY"); diff --git a/src/gui/Calendar/index.tsx b/src/gui/Calendar/index.tsx index 375ea76..1868654 100644 --- a/src/gui/Calendar/index.tsx +++ b/src/gui/Calendar/index.tsx @@ -15,6 +15,8 @@ import useGetMonthStatistics from "../hooks/useGetMonthStatistics"; import MsgType from "@constants/messageTypes"; import { PluginPostMessage } from "@constants/pluginMessageTypes"; import useWebviewApiOnMessage from "../hooks/useWebViewApiOnMessage"; +import useOnSettingsChange from "../hooks/useOnSettingsChange"; +import { WEEK_START_DAY, WeekStartDay } from "@constants/Settings"; const DAYS_IN_A_WEEK = 7; const CALENDAR_ROWS = 6; @@ -78,16 +80,37 @@ function Calendar({ [onKeyboardNavigation] ); + const weekStartDay = useOnSettingsChange<WeekStartDay>( + WEEK_START_DAY, + WeekStartDay.Sunday + ); + const currentMonthFirstDay = shownMonth.startOf("month"); const calendarBody: React.JSX.Element[] = []; - const firstRowOffset = -currentMonthFirstDay.weekday(); + let firstRowBacktrackOffset; + if (weekStartDay === WeekStartDay.Monday) { + // If the current month starts on a day other than Monday, we need to backtrack. + // Monday has isoWeek 1, and we need to backtrack 0 days. + // Tuesday has isoWeek 2, and we need to backtrack 1 days. + // ... + // Sunday has isoWeek 7, and we need to backtrack 6 days. + firstRowBacktrackOffset = currentMonthFirstDay.isoWeekday() - 1; + } else { + // Fallback to asssuming week starts on Sunday. + // If current month starts on a day other than Sunday, we need to backtrack. + // Monday has isoWeek 1, and we need to backtrack 1 day. + // Tuesday has isoWeek 2, and we need to backtrack 2 days. + // ... + // Sunday has isoWeek 7, and we need to backtrack no days. + firstRowBacktrackOffset = currentMonthFirstDay.isoWeekday() % 7; + } // Note: Moment JS uses in place operations const workingDate = currentMonthFirstDay.clone(); // Offset to fill in dates from previous month till first of current month. - workingDate.add(firstRowOffset, "days"); + workingDate.subtract(firstRowBacktrackOffset, "days"); for (let row = 0; row < CALENDAR_ROWS; row++) { const cols: React.JSX.Element[] = [];