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

chore: replace moment with luxon in calendar-view #1065

Merged
merged 15 commits into from Aug 6, 2021
125 changes: 125 additions & 0 deletions packages/dendron-next-server/lib/luxon.ts
@@ -0,0 +1,125 @@
// from https://github.com/react-component/picker/pull/230

import { DateTime, Info } from "luxon";
import type { GenerateConfig } from "rc-picker/lib/generate";

/**
* Normalizes part of a moment format string that should
* not be escaped to a luxon compatible format string.
*
* @param part string
* @returns string
*/
const normalizeFormatPart = (part: string): string =>
part
.replace(/Y/g, "y")
.replace(/D/g, "d")
.replace(/gg/g, "kk")
.replace(/Q/g, "q")
.replace(/([Ww])o/g, "WW");

/**
* Normalizes a moment compatible format string to a luxon compatible format string
*
* @param format string
* @returns string
*/
const normalizeFormat = (format: string): string =>
format
// moment escapes strings contained in brackets
.split(/[[\]]/)
.map((part, index) => {
const shouldEscape = index % 2 > 0;

return shouldEscape ? part : normalizeFormatPart(part);
})
// luxon escapes strings contained in single quotes
.join("'");

/**
* Normalizes language tags used to luxon compatible
* language tags by replacing underscores with hyphen-minus.
*
* @param locale string
* @returns string
*/
const normalizeLocale = (locale: string): string => locale.replace(/_/g, "-");

const generateConfig: GenerateConfig<DateTime> = {
// get
getNow: () => DateTime.local(),
getFixedDate: (string) => DateTime.fromFormat(string, "yyyy-MM-dd"),
getEndDate: (date) => date.endOf("month"),
getWeekDay: (date) => date.weekday,
getYear: (date) => date.year,
getMonth: (date) => date.month - 1, // getMonth should return 0-11, luxon month returns 1-12
getDate: (date) => date.day,
getHour: (date) => date.hour,
getMinute: (date) => date.minute,
getSecond: (date) => date.second,

// set
addYear: (date, diff) => date.plus({ year: diff }),
addMonth: (date, diff) => date.plus({ month: diff }),
addDate: (date, diff) => date.plus({ day: diff }),
setYear: (date, year) => date.set({ year }),
setMonth: (date, month) => date.set({ month: month + 1 }), // setMonth month argument is 0-11, luxon months are 1-12
setDate: (date, day) => date.set({ day }),
setHour: (date, hour) => date.set({ hour }),
setMinute: (date, minute) => date.set({ minute }),
setSecond: (date, second) => date.set({ second }),

// Compare
isAfter: (date1, date2) => date1 > date2,
isValidate: (date) => date.isValid,

locale: {
getWeekFirstDate: (locale, date) =>
date.setLocale(normalizeLocale(locale)).startOf("week"),
getWeekFirstDay: (locale) =>
DateTime.local().setLocale(normalizeLocale(locale)).startOf("week")
.weekday,
getWeek: (locale, date) =>
date.setLocale(normalizeLocale(locale)).weekNumber,
getShortWeekDays: (locale) => {
const weekdays = Info.weekdays("short", {
locale: normalizeLocale(locale),
});

// getShortWeekDays should return weekday labels starting from Sunday.
// luxon returns them starting from Monday, so we have to shift the results.
weekdays.unshift(weekdays.pop() as string);

return weekdays
},
getShortMonths: (locale) =>
Info.months("short", { locale: normalizeLocale(locale) }),
// @ts-ignore -- allow format to return `null`
format: (locale, date, format) => {
if (!date || !date.isValid) {
return null;
}

return date
.setLocale(normalizeLocale(locale))
.toFormat(normalizeFormat(format));
},
parse: (locale, text, formats) => {
for (let i = 0; i < formats.length; i += 1) {
const normalizedFormat = normalizeFormat(formats[i]);

const date = DateTime.fromFormat(text, normalizedFormat, {
locale: normalizeLocale(locale),
});

if (date.isValid) {
return date;
}
}

return null;
},
},
};

export default generateConfig;
82 changes: 44 additions & 38 deletions packages/dendron-next-server/pages/vscode/calendar-view.tsx
Expand Up @@ -2,6 +2,7 @@ import {
CalendarViewMessageType,
DMessageSource,
NoteProps,
Time,
} from "@dendronhq/common-all";
import {
createLogger,
Expand All @@ -18,32 +19,35 @@ import { Badge, ConfigProvider } from "antd";
import generateCalendar from "antd/lib/calendar/generateCalendar";
import classNames from "classnames";
import _ from "lodash";
import type { Moment } from "moment";
import moment from "moment";
import momentGenerateConfig from "rc-picker/lib/generate/moment";
import React, { useCallback, useEffect, useMemo, useState } from "react";
import luxonGenerateConfig from "../../lib/luxon";
import { DendronProps } from "../../lib/types";

function isSameYear(date1: Moment, date2: Moment) {
type DateTime = InstanceType<typeof Time.DateTime>;

const Calendar = generateCalendar<DateTime>(luxonGenerateConfig);

type CalendarProps = AntdCalendarProps<DateTime>;

function isSameYear(date1: DateTime, date2: DateTime) {
return (
date1 &&
date2 &&
momentGenerateConfig.getYear(date1) === momentGenerateConfig.getYear(date2)
luxonGenerateConfig.getYear(date1) === luxonGenerateConfig.getYear(date2)
);
}

function isSameMonth(date1: Moment, date2: Moment) {
function isSameMonth(date1: DateTime, date2: DateTime) {
return (
isSameYear(date1, date2) &&
momentGenerateConfig.getMonth(date1) ===
momentGenerateConfig.getMonth(date2)
luxonGenerateConfig.getMonth(date1) === luxonGenerateConfig.getMonth(date2)
);
}

function isSameDate(date1: Moment, date2: Moment) {
function isSameDate(date1: DateTime, date2: DateTime) {
return (
isSameMonth(date1, date2) &&
momentGenerateConfig.getDate(date1) === momentGenerateConfig.getDate(date2)
luxonGenerateConfig.getDate(date1) === luxonGenerateConfig.getDate(date2)
);
}

Expand All @@ -52,12 +56,9 @@ function getMaybeDatePortion({ fname }: NoteProps, journalName: string) {
return fname.slice(journalIndex + journalName.length + 1);
}

const today = momentGenerateConfig.getNow();
const Calendar = generateCalendar<Moment>(momentGenerateConfig);
const today = luxonGenerateConfig.getNow();
const { EngineSliceUtils } = engineSlice;

type CalendarProps = AntdCalendarProps<Moment>;

function CalendarView({ engine, ide }: DendronProps) {
// --- init
const ctx = "CalendarView";
Expand All @@ -80,23 +81,21 @@ function CalendarView({ engine, ide }: DendronProps) {

const maxDots: number = 5;
const wordsPerDot: number = 250;
const dailyJournalDomain = config?.journal.dailyDomain;
const defaultJournalName = config?.journal.name;
let defaultJournalDateFormat = config?.journal.dateFormat;
const dayOfWeek = config?.journal.firstDayOfWeek;
const locale = "en-us";
const dailyJournalDomain = config?.journal.dailyDomain || "daily";
const defaultJournalName = config?.journal.name || "journal";

// luxon token format lookup https://github.com/moment/luxon/blob/master/docs/formatting.md#table-of-tokens
let defaultJournalDateFormat = config?.journal.dateFormat || "y.MM.dd";
const defaultJournalMonthDateFormat = "y.MM"; // TODO compute format for currentMode="year" from config

// Currently luxon does not support setting first day of the week (https://github.com/moment/luxon/issues/373)
// const dayOfWeek = config?.journal.firstDayOfWeek;
// const locale = "en-us";

if (defaultJournalDateFormat) {
defaultJournalDateFormat = defaultJournalDateFormat.replace(/dd/, "DD");
defaultJournalDateFormat = defaultJournalDateFormat.replace(/DD/, "dd");
}

useEffect(() => {
moment.updateLocale(locale, {
week: {
dow: dayOfWeek!,
},
});
}, [dayOfWeek, locale]);

const groupedDailyNotes = useMemo(() => {
const vaultNotes = _.values(notes).filter((notes) => {
if (currentVault) {
Expand All @@ -109,7 +108,7 @@ function CalendarView({ engine, ide }: DendronProps) {
note.fname.startsWith(`${dailyJournalDomain}.`)
);
const result = _.groupBy(dailyNotes, (note) => {
return getMaybeDatePortion(note, defaultJournalName!);
return getMaybeDatePortion(note, defaultJournalName);
});
return result;
}, [notes, defaultJournalName, currentVault?.fsPath]);
Expand All @@ -118,30 +117,37 @@ function CalendarView({ engine, ide }: DendronProps) {
if (noteActive) {
const maybeDatePortion = getMaybeDatePortion(
noteActive,
defaultJournalName!
defaultJournalName
);

// check if daily file is `y.MM` instead of `y.MM.dd` to apply proper format string.
// unlike moment luxon marks the date as invalid if date and dateformat do not match
const isMonthly = maybeDatePortion.split(".").length === 2;

return maybeDatePortion && _.first(groupedDailyNotes[maybeDatePortion])
? moment(maybeDatePortion, defaultJournalDateFormat)
? Time.DateTime.fromFormat(
maybeDatePortion,
isMonthly ? defaultJournalMonthDateFormat : defaultJournalDateFormat
)
: undefined;
}
}, [noteActive, groupedDailyNotes]);

const getDateKey = useCallback<
(date: Moment, mode?: CalendarProps["mode"]) => string
(date: DateTime, mode?: CalendarProps["mode"]) => string
>(
(date, mode) => {
const format =
(mode || activeMode) === "month"
? defaultJournalDateFormat || "y.MM.dd"
: "y.MM"; // TODO compute format for currentMode="year" from config
return date.format(format);
: defaultJournalMonthDateFormat;
return date.toFormat(format);
},
[activeMode, defaultJournalDateFormat]
);

const onSelect = useCallback<
(date: Moment, mode?: CalendarProps["mode"]) => void
(date: DateTime, mode?: CalendarProps["mode"]) => void
>(
(date, mode) => {
logger.info({ ctx: "onSelect", date });
Expand Down Expand Up @@ -170,7 +176,7 @@ function CalendarView({ engine, ide }: DendronProps) {
const onClickToday = useCallback(() => {
const mode = "month";
setActiveMode(mode);
onSelect(moment(), mode);
onSelect(Time.now(), mode);
}, [onSelect]);

const dateFullCellRender = useCallback<
Expand Down Expand Up @@ -240,7 +246,7 @@ function CalendarView({ engine, ide }: DendronProps) {
className={`${calendarPrefixCls}-date-value`}
style={{ color: !dailyNote ? "gray" : undefined }}
>
{_.padStart(String(momentGenerateConfig.getDate(date)), 2, "0")}
{_.padStart(String(luxonGenerateConfig.getDate(date)), 2, "0")}
</div>
<div className={`${calendarPrefixCls}-date-content`}>{dateCell}</div>
</div>
Expand Down Expand Up @@ -303,7 +309,7 @@ function CalendarView({ engine, ide }: DendronProps) {
}

function areEqual(prevProps: DendronProps, nextProps: DendronProps) {
const logger = createLogger("treeViewContainer");
const logger = createLogger("calendarViewContainer");
const isDiff = _.some([
// active note changed
prevProps.ide.noteActive?.id !== nextProps.ide.noteActive?.id,
Expand Down