From 3c81fba0b61699c483d2389c39c06672a4806f48 Mon Sep 17 00:00:00 2001 From: Maarten den Braber Date: Sun, 26 Jan 2025 00:09:45 +0100 Subject: [PATCH 1/7] Fix date on new fields --- src/ui/components/FieldControl/FieldControl.svelte | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/ui/components/FieldControl/FieldControl.svelte b/src/ui/components/FieldControl/FieldControl.svelte index e427c22d..4b522b15 100644 --- a/src/ui/components/FieldControl/FieldControl.svelte +++ b/src/ui/components/FieldControl/FieldControl.svelte @@ -76,11 +76,11 @@ on:change={({ detail: value }) => (cachedValue = value)} on:blur={() => { if (!cachedValue) { - onChange(cachedValue); + onChange(); return; } const cachedDate = dayjs(cachedValue); - const newDatetime = dayjs(isDate(value) ? value : null) + const newDatetime = dayjs(isDate(value) ? value : cachedValue) .set("year", cachedDate.year()) .set("month", cachedDate.month()) .set("date", cachedDate.date()); From 71c3574bbf8bd647927d192d9304e7dbebe184e6 Mon Sep 17 00:00:00 2001 From: Chiyu Liang <532117255@qq.com> Date: Sat, 25 Jan 2025 20:23:43 -0500 Subject: [PATCH 2/7] Fix date input Add input event handler call to DateInput Shift to local DateInput component Reserve submitting null when no cache Simplified date / datetime input Clean up --- .../viewOptions/color/ColorOptions.svelte | 3 +- .../viewOptions/filter/FilterOptions.svelte | 3 +- src/ui/components/DateInput.svelte | 66 +++++++++++++++++++ .../FieldControl/FieldControl.svelte | 9 +-- src/ui/modals/components/CreateField.svelte | 3 +- .../GridCell/GridDateCell/GridDateCell.svelte | 10 +-- .../GridDatetimeCell/GridDatetimeCell.svelte | 4 +- 7 files changed, 85 insertions(+), 13 deletions(-) create mode 100644 src/ui/components/DateInput.svelte diff --git a/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte b/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte index 49231f49..874fa346 100644 --- a/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte +++ b/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte @@ -11,9 +11,10 @@ NumberInput, ColorInput, Checkbox, - DateInput, + // DateInput, // DatetimeInput, } from "obsidian-svelte"; + import DateInput from "src/ui/components/DateInput.svelte"; import DatetimeInput from "src/ui/components/DatetimeInput.svelte"; import { TagsInput } from "src/ui/components/TagsInput"; import HorizontalGroup from "src/ui/components/HorizontalGroup/HorizontalGroup.svelte"; diff --git a/src/ui/app/toolbar/viewOptions/filter/FilterOptions.svelte b/src/ui/app/toolbar/viewOptions/filter/FilterOptions.svelte index 21a2144e..ca4ef151 100644 --- a/src/ui/app/toolbar/viewOptions/filter/FilterOptions.svelte +++ b/src/ui/app/toolbar/viewOptions/filter/FilterOptions.svelte @@ -9,9 +9,10 @@ TextInput, NumberInput, Checkbox, - DateInput, + // DateInput, // DatetimeInput, } from "obsidian-svelte"; + import DateInput from "src/ui/components/DateInput.svelte"; import DatetimeInput from "src/ui/components/DatetimeInput.svelte"; import { TagsInput } from "src/ui/components/TagsInput"; import HorizontalGroup from "src/ui/components/HorizontalGroup/HorizontalGroup.svelte"; diff --git a/src/ui/components/DateInput.svelte b/src/ui/components/DateInput.svelte new file mode 100644 index 00000000..e7ca4a42 --- /dev/null +++ b/src/ui/components/DateInput.svelte @@ -0,0 +1,66 @@ + + + + + diff --git a/src/ui/components/FieldControl/FieldControl.svelte b/src/ui/components/FieldControl/FieldControl.svelte index 4b522b15..19121ff7 100644 --- a/src/ui/components/FieldControl/FieldControl.svelte +++ b/src/ui/components/FieldControl/FieldControl.svelte @@ -1,12 +1,13 @@ diff --git a/src/ui/components/DatetimeInput.svelte b/src/ui/components/DatetimeInput.svelte index 42a92d58..956797d0 100644 --- a/src/ui/components/DatetimeInput.svelte +++ b/src/ui/components/DatetimeInput.svelte @@ -28,17 +28,6 @@ ); } } - - function handleInput(event: Event) { - if (event.currentTarget instanceof HTMLInputElement) { - dispatch( - "input", - event.currentTarget.value - ? Temporal.PlainDateTime.from(event.currentTarget.value) - : null - ); - } - } diff --git a/src/ui/components/FieldControl/FieldControl.svelte b/src/ui/components/FieldControl/FieldControl.svelte index 3ab498b3..974a1fd5 100644 --- a/src/ui/components/FieldControl/FieldControl.svelte +++ b/src/ui/components/FieldControl/FieldControl.svelte @@ -70,7 +70,7 @@ {#if field.typeConfig?.time} { + on:change={({ detail: value }) => { if (value) { cachedValue = cachedValue ? cachedValue.with({ @@ -92,21 +92,6 @@ { - console.log("change event"); - if (value) { - cachedValue = cachedValue - ? cachedValue.with({ - year: value.year, - month: value.month, - day: value.day, - }) - : value.toZonedDateTime(Temporal.Now.timeZoneId()); // to local timezone - } else { - cachedValue = null; - } - }} - on:input={({ detail: value }) => { - console.log("input event"); //TODO: awaiting debugging if (value) { cachedValue = cachedValue ? cachedValue.with({ diff --git a/src/ui/views/Calendar/CalendarView.svelte b/src/ui/views/Calendar/CalendarView.svelte index 8b201642..eb25fab2 100644 --- a/src/ui/views/Calendar/CalendarView.svelte +++ b/src/ui/views/Calendar/CalendarView.svelte @@ -109,15 +109,14 @@ function handleRecordChange(date: Temporal.PlainDate, record: DataRecord) { if (dateField) { if (dateField.type === DataFieldType.Date) { - const newDatetime = Temporal.PlainDateTime.from(record.values[dateField.name] as string) - .with({year: date.year, - month: date.month, - day: date.day})//check intension here + const newDatetime = Temporal.PlainDateTime.from( + record.values[dateField.name] as string + ).with({ year: date.year, month: date.month, day: date.day }); //check intension here api.updateRecord( updateRecordValues(record, { - [dateField.name]: - dateField.typeConfig?.time ? newDatetime.toString({smallestUnit: "minute"}) : newDatetime.toPlainDate().toString() - , + [dateField.name]: dateField.typeConfig?.time + ? newDatetime.toString({ smallestUnit: "minute" }) + : newDatetime.toPlainDate().toString(), }), fields ); @@ -249,10 +248,14 @@ {#each weekDays as weekDay} {$i18n.t("views.calendar.weekday", { - value: new Date(weekDay.toString()), // bug may arise here, use local Date variable + value: new Date( + weekDay.toZonedDateTime( + Temporal.Now.timeZoneId() + ).epochMilliseconds + ), // bug may arise here, use local Date variable formatParams: { value: { weekday: "short" }, }, diff --git a/src/ui/views/Calendar/calendar.test.ts b/src/ui/views/Calendar/calendar.test.ts new file mode 100644 index 00000000..fa6a8b02 --- /dev/null +++ b/src/ui/views/Calendar/calendar.test.ts @@ -0,0 +1,6 @@ +import { describe, expect, it } from "@jest/globals"; +import { startOfWeek, endOfWeek, getFirstDayOfWeek } from "./calendar"; + +describe("generateDateRange", () => { + it("month range", () => {}); +}); diff --git a/src/ui/views/Calendar/calendar.ts b/src/ui/views/Calendar/calendar.ts index 3b609560..69ad9151 100644 --- a/src/ui/views/Calendar/calendar.ts +++ b/src/ui/views/Calendar/calendar.ts @@ -27,15 +27,15 @@ export function addInterval( ): Temporal.PlainDate { switch (interval) { case "month": - return date.add({months: 1}); + return date.add({ months: 1 }); case "2weeks": - return date.add({weeks: 2}); + return date.add({ weeks: 2 }); case "week": - return date.add({weeks: 1}); + return date.add({ weeks: 1 }); case "3days": - return date.add({days: 1}); //TODO: double check + return date.add({ days: 1 }); //TODO: double check case "day": - return date.add({days: 1}); + return date.add({ days: 1 }); } } @@ -45,15 +45,15 @@ export function subtractInterval( ): Temporal.PlainDate { switch (interval) { case "month": - return date.subtract({months: 1}); + return date.subtract({ months: 1 }); case "2weeks": - return date.subtract({weeks: 2}); + return date.subtract({ weeks: 2 }); case "week": - return date.subtract({weeks: 1}); + return date.subtract({ weeks: 1 }); case "3days": - return date.subtract({days: 1}); //TODO: double check + return date.subtract({ days: 1 }); //TODO: double check case "day": - return date.subtract({days: 1}); + return date.subtract({ days: 1 }); } } @@ -66,11 +66,7 @@ export function groupRecordsByField( records.forEach((record) => { const dateValue = record.values[field]; - const start = dateValue - ? isDate(dateValue) - ? dateValue - : null - : null; + const start = dateValue ? (isDate(dateValue) ? dateValue : null) : null; if (start) { const dateStr = start.toPlainDate().toString(); @@ -95,24 +91,30 @@ export function computeDateInterval( switch (interval) { case "month": return [ - startOfWeek(anchor.with({day: 1}), firstDayOfWeek), - endOfWeek(anchor.with({day: anchor.daysInMonth}), firstDayOfWeek), + startOfWeek(anchor.with({ day: 1 }), firstDayOfWeek), + endOfWeek(anchor.with({ day: anchor.daysInMonth }), firstDayOfWeek), ]; case "2weeks": - return [sow, eow.add({weeks: 1})]; + return [sow, eow.add({ weeks: 1 })]; case "week": return [sow, eow]; case "3days": - return [anchor, anchor.add({days: 2})]; + return [anchor, anchor.add({ days: 2 })]; case "day": return [anchor, anchor]; } } -export function generateTitle(dateInterval: [Temporal.PlainDate, Temporal.PlainDate]) { +export function generateTitle( + dateInterval: [Temporal.PlainDate, Temporal.PlainDate] +) { if (dateInterval[0].equals(dateInterval[1])) { return get(i18n).t("views.calendar.date", { - value: new Date(dateInterval[0].toString()), + value: new Date( + dateInterval[0].toZonedDateTime( + Temporal.Now.timeZoneId() + ).epochMilliseconds + ), formatParams: { value: { year: "numeric", month: "long", day: "numeric" }, }, @@ -121,10 +123,22 @@ export function generateTitle(dateInterval: [Temporal.PlainDate, Temporal.PlainD if (dateInterval[0].year === dateInterval[1].year) { return get(i18n).t("views.calendar.interval", { - from: new Date(dateInterval[0].toString()), - to: new Date(dateInterval[1].toString()), + from: new Date( + dateInterval[0].toZonedDateTime( + Temporal.Now.timeZoneId() + ).epochMilliseconds + ), + to: new Date( + dateInterval[1].toZonedDateTime( + Temporal.Now.timeZoneId() + ).epochMilliseconds + ), en_separator: ", ", - custom_year: new Date(dateInterval[0].toString()), + custom_year: new Date( + dateInterval[0].toZonedDateTime( + Temporal.Now.timeZoneId() + ).epochMilliseconds + ), formatParams: { from: { month: "short", day: "numeric" }, to: { month: "short", day: "numeric" }, @@ -134,8 +148,16 @@ export function generateTitle(dateInterval: [Temporal.PlainDate, Temporal.PlainD } return get(i18n).t("views.calendar.interval", { - from: new Date(dateInterval[0].toString()), - to: new Date(dateInterval[1].toString()), + from: new Date( + dateInterval[0].toZonedDateTime( + Temporal.Now.timeZoneId() + ).epochMilliseconds + ), + to: new Date( + dateInterval[1].toZonedDateTime( + Temporal.Now.timeZoneId() + ).epochMilliseconds + ), en_separator: "", custom_year: "", formatParams: { @@ -151,10 +173,10 @@ export function generateDates( ): Temporal.PlainDate[] { const dates: Temporal.PlainDate[] = []; - const numDays = dateInterval[0].until(dateInterval[1]).days + const numDays = dateInterval[0].until(dateInterval[1]).days; for (let i = 0; i <= numDays; i++) { - dates.push(dateInterval[0].add({days: i})); + dates.push(dateInterval[0].add({ days: i })); } return dates; @@ -194,16 +216,16 @@ export function startOfWeek( date: Temporal.PlainDate, firstDayOfWeek: number ): Temporal.PlainDate { - const offset = (7 + date.day - firstDayOfWeek) % 7; - return date.subtract({days: offset}); + const offset = (7 + date.dayOfWeek - firstDayOfWeek) % 7; + return date.subtract({ days: offset }); } export function endOfWeek( date: Temporal.PlainDate, firstDayOfWeek: number ): Temporal.PlainDate { - const offset = (firstDayOfWeek + 6 - date.day) % 7; - return date.add({days: offset}); + const offset = (firstDayOfWeek + 6 - date.dayOfWeek) % 7; + return date.add({ days: offset }); } export type LocaleOption = "system" | "obsidian"; diff --git a/src/ui/views/Calendar/components/Calendar/Day.svelte b/src/ui/views/Calendar/components/Calendar/Day.svelte index d0986d1c..e62af96e 100644 --- a/src/ui/views/Calendar/components/Calendar/Day.svelte +++ b/src/ui/views/Calendar/components/Calendar/Day.svelte @@ -47,7 +47,7 @@ */ export let onRecordAdd: () => void; - $: weekend = date.day === 0 || date.day === 6; + $: weekend = date.dayOfWeek % 7 === 0 || date.dayOfWeek === 6; $: today = date.equals(Temporal.Now.plainDateISO()); function handleDblClick(event: MouseEvent) { From dd2365543389094bc7ad1e2e35a3ea9f9dd0c824 Mon Sep 17 00:00:00 2001 From: Chiyu Liang <532117255@qq.com> Date: Sun, 26 Jan 2025 05:24:30 -0500 Subject: [PATCH 7/7] Introduce temporal Write date string back according to timezone Only reserve non-local timezone marks, non-DST save. Possible solution should be providing settings to specify user zone Add hint Resume input events on date inputs Extend date string support for timezoneId Fix date parsing Remove dayjs Cancel default date filling in field creation Refactor date string parsing Clean up redundant comments and tests Fix bugs on filters and card display Fix calendar date range Refactor calendar with Temporal Seems buggy, needs unit test to calendar helpers Refactor using Temporal Stage 1: Handle most views and sorts, except for calendar --- package-lock.json | 19 +- package.json | 4 +- src/lib/dataApi.ts | 35 +- src/lib/dataframe/dataframe.ts | 55 +-- src/lib/datasources/dataview/standardize.ts | 4 +- src/lib/datasources/helpers.ts | 51 ++- src/main.ts | 6 - src/ui/app/filterFunctions.ts | 50 ++- src/ui/app/onboarding/demoProject.ts | 14 +- .../viewOptions/color/ColorOptions.svelte | 10 +- .../viewOptions/filter/FilterOptions.svelte | 10 +- src/ui/app/viewSort.test.ts | 337 +++++++++++++++--- src/ui/app/viewSort.ts | 17 +- src/ui/components/CardMetadata/Date.svelte | 7 +- .../components/CardMetadata/Datetime.svelte | 7 +- src/ui/components/DateInput.svelte | 15 +- src/ui/components/DatetimeInput.svelte | 14 +- .../FieldControl/FieldControl.svelte | 57 ++- src/ui/modals/components/CreateField.svelte | 78 ++-- src/ui/views/Calendar/CalendarView.svelte | 35 +- src/ui/views/Calendar/calendar.ts | 130 ++++--- .../Calendar/components/Calendar/Day.svelte | 10 +- .../GridCell/GridDateCell/GridDateCell.svelte | 65 +++- .../GridDatetimeCell/GridDatetimeCell.svelte | 73 +++- 24 files changed, 801 insertions(+), 302 deletions(-) diff --git a/package-lock.json b/package-lock.json index d5b5502b..3ca782dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,7 +23,6 @@ "@typescript-eslint/eslint-plugin": "^8.16.0", "@typescript-eslint/parser": "^8.16.0", "builtin-modules": "^3.3.0", - "dayjs": "^1.11.11", "esbuild": "^0.16.17", "esbuild-jest": "^0.5.0", "esbuild-plugin-replace": "^1.4.0", @@ -47,6 +46,7 @@ "svelte-i18next": "^2.2.2", "svelte-media-queries": "^1.6.2", "svelte-preprocess": "^5.1.4", + "temporal-polyfill": "^0.2.5", "ts-essentials": "^10.0.3", "ts-jest": "^29.1.3", "tslib": "^2.6.2", @@ -7620,6 +7620,23 @@ "@jridgewell/sourcemap-codec": "^1.5.0" } }, + "node_modules/temporal-polyfill": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/temporal-polyfill/-/temporal-polyfill-0.2.5.tgz", + "integrity": "sha512-ye47xp8Cb0nDguAhrrDS1JT1SzwEV9e26sSsrWzVu+yPZ7LzceEcH0i2gci9jWfOfSCCgM3Qv5nOYShVUUFUXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "temporal-spec": "^0.2.4" + } + }, + "node_modules/temporal-spec": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/temporal-spec/-/temporal-spec-0.2.4.tgz", + "integrity": "sha512-lDMFv4nKQrSjlkHKAlHVqKrBG4DyFfa9F74cmBZ3Iy3ed8yvWnlWSIdi4IKfSqwmazAohBNwiN64qGx4y5Q3IQ==", + "dev": true, + "license": "ISC" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmmirror.com/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/package.json b/package.json index 1c0aecb3..8df699f3 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,6 @@ "@typescript-eslint/eslint-plugin": "^8.16.0", "@typescript-eslint/parser": "^8.16.0", "builtin-modules": "^3.3.0", - "dayjs": "^1.11.11", "esbuild": "^0.16.17", "esbuild-jest": "^0.5.0", "esbuild-plugin-replace": "^1.4.0", @@ -56,6 +55,7 @@ "svelte-i18next": "^2.2.2", "svelte-media-queries": "^1.6.2", "svelte-preprocess": "^5.1.4", + "temporal-polyfill": "^0.2.5", "ts-essentials": "^10.0.3", "ts-jest": "^29.1.3", "tslib": "^2.6.2", @@ -63,4 +63,4 @@ "uuid": "^11.0.3", "yaml": "^2.4.2" } -} \ No newline at end of file +} diff --git a/src/lib/dataApi.ts b/src/lib/dataApi.ts index bac957d8..75c94530 100644 --- a/src/lib/dataApi.ts +++ b/src/lib/dataApi.ts @@ -1,4 +1,3 @@ -import dayjs from "dayjs"; import { produce } from "immer"; import moment from "moment"; import { get } from "svelte/store"; @@ -17,6 +16,7 @@ import { decodeFrontMatter, encodeFrontMatter } from "./metadata"; import { i18n } from "./stores/i18n"; import { settings } from "./stores/settings"; import { interpolateTemplate } from "./templates/interpolate"; +import { Temporal } from "temporal-polyfill"; import { function as F, task as T, either as E, taskEither as TE } from "fp-ts"; import { @@ -181,21 +181,38 @@ export function doUpdateRecord( Object.entries({ ...frontmatter, ...record.values }) .map((entry) => { if (isDate(entry[1])) { - const isDatetime = fields.find( + const hasTime = fields.some( (field) => field.name === entry[0] && field.type === DataFieldType.Date && (field.typeConfig?.time || - entry[1].getHours() || - entry[1].getMinutes() || - entry[1].getSeconds() || - entry[1].getMilliseconds()) + entry[1].hour || + entry[1].minute || + entry[1].second) ); + const zoned = + entry[1].timeZoneId !== Temporal.Now.timeZoneId() && // default "+08:00" comparing to "America/New York" + entry[1].offset !== Temporal.Now.zonedDateTimeISO().offset; // default "+08:00" comparing to "-05:00" + + // const hasOffset = + const utc = entry[1].timeZoneId === "UTC"; + //TODO; format with Z mark, seems hard to do w/0 third-party libs + return produce(entry, (draft) => { - draft[1] = dayjs(entry[1]).format( - isDatetime ? "YYYY-MM-DDTHH:mm" : "YYYY-MM-DD" - ); + draft[1] = hasTime + ? zoned + ? (entry[1] as Temporal.ZonedDateTime).toString({ + smallestUnit: "minute", + offset: "auto", + timeZoneName: "never", + }) + : (entry[1] as Temporal.ZonedDateTime) + .toPlainDateTime() + .toString({ smallestUnit: "minute" }) + : (entry[1] as Temporal.ZonedDateTime) + .toPlainDate() + .toString(); }); } return entry; diff --git a/src/lib/dataframe/dataframe.ts b/src/lib/dataframe/dataframe.ts index ed77a55e..5f827a87 100644 --- a/src/lib/dataframe/dataframe.ts +++ b/src/lib/dataframe/dataframe.ts @@ -1,5 +1,6 @@ import type { FieldConfig } from "src/settings/settings"; import type { RecordError } from "../datasources/frontmatter/datasource"; +import { Temporal } from "temporal-polyfill"; /** * DataFrame is the core data structure that contains structured data for a @@ -77,32 +78,32 @@ export type DataValue = | string | number | boolean - | Date + | Temporal.ZonedDateTime | Array>; -export function isOptionalDataValue( - value: unknown -): value is Optional { - switch (typeof value) { - case "string": - return true; - case "number": - return true; - case "boolean": - return true; - default: - return false; - } -} +// export function isOptionalDataValue( +// value: unknown +// ): value is Optional { +// switch (typeof value) { +// case "string": +// return true; +// case "number": +// return true; +// case "boolean": +// return true; +// default: +// return false; +// } +// } -export function isRepeatedDataValue( - value: unknown -): value is Array> { - if (Array.isArray(value)) { - return value.every(isOptionalDataValue); - } - return false; -} +// export function isRepeatedDataValue( +// value: unknown +// ): value is Array> { +// if (Array.isArray(value)) { +// return value.every(isOptionalDataValue); +// } +// return false; +// } export type Optional = | T @@ -138,8 +139,10 @@ export function isNumber( return typeof value === "number"; } -export function isDate(value: Optional | DataValue): value is Date { - return value instanceof Date; +export function isDate( + value: Optional | DataValue +): value is Temporal.ZonedDateTime { + return value instanceof Temporal.ZonedDateTime; } // export function hasValue(value: Optional): value is DataValue { @@ -179,7 +182,7 @@ export function isOptionalNumber( export function isOptionalDate( value: Optional -): value is Optional { +): value is Optional { return isDate(value) || isOptional(value); } diff --git a/src/lib/datasources/dataview/standardize.ts b/src/lib/datasources/dataview/standardize.ts index 2e219ab1..fb5ade2a 100644 --- a/src/lib/datasources/dataview/standardize.ts +++ b/src/lib/datasources/dataview/standardize.ts @@ -1,6 +1,6 @@ -import dayjs from "dayjs"; import type { Link } from "obsidian-dataview"; import type { DataValue, Optional } from "src/lib/dataframe/dataframe"; +import { Temporal } from "temporal-polyfill"; /** * standardizeValues converts a Dataview data structure of values to the common @@ -36,6 +36,6 @@ function standardizeObject(value: any) { return (value as Link).toString(); } if ("ts" in value) { - return dayjs(value.ts).format("YYYY-MM-DD"); + return Temporal.PlainDateTime.from(value.c).toString() } } diff --git a/src/lib/datasources/helpers.ts b/src/lib/datasources/helpers.ts index d29283f4..a4bf0a54 100644 --- a/src/lib/datasources/helpers.ts +++ b/src/lib/datasources/helpers.ts @@ -1,5 +1,3 @@ -import dayjs from "dayjs"; - import { DataFieldType, type DataField, @@ -7,6 +5,7 @@ import { type DataValue, type Optional, } from "../dataframe/dataframe"; +import { Temporal } from "temporal-polyfill"; /** * Parses the values for each record based on the detected field types. @@ -31,7 +30,9 @@ export function parseRecords( switch (field.type) { case DataFieldType.Date: if (typeof value === "string") { - record.values[field.name] = dayjs(value).toDate(); + record.values[field.name] = parseStringDate(value); + } else if (typeof value === "number") { + record.values[field.name] = parseNumberDate(value); } break; case DataFieldType.Number: @@ -46,7 +47,7 @@ export function parseRecords( break; case DataFieldType.String: if (typeof value !== "object") { - record.values[field.name] = value?.toLocaleString(); + record.values[field.name] = value?.toLocaleString(); //TODO: refine this } break; } @@ -55,6 +56,44 @@ export function parseRecords( return records; } +function parseStringDate(value: string) { + for (const parseFn of [ + () => Temporal.ZonedDateTime.from(value), // with timezone id + () => Temporal.Instant.from(value).toZonedDateTimeISO(value), // with timezone offset + () => + Temporal.PlainDateTime.from(value).toZonedDateTime( + Temporal.Now.timeZoneId() + ), // w/o timezone info + ]) { + try { + return parseFn(); + } catch { + continue; + } + } + return null; +} + +function parseNumberDate(value: number) { + for (const parseFn of [ + () => + Temporal.PlainDateTime.from(value.toString()).toZonedDateTime( + Temporal.Now.timeZoneId() + ), + () => + Temporal.Instant.fromEpochMilliseconds(value).toZonedDateTimeISO( + Temporal.Now.timeZoneId() + ), + ]) { + try { + return parseFn(); + } catch { + continue; + } + } + return null; +} + /** * Merges a new version of `values` into a copy of data record. * @@ -123,7 +162,9 @@ export function detectCellType(value: unknown): DataFieldType { // Standard types if (typeof value === "string") { if ( - /^\d{4}-\d{2}-\d{2}(T)?(\d{2})?(:\d{2})?(:\d{2})?(.\d{3})?$/.test(value) + /^\d{4}-\d{2}-\d{2}(?:[Tt ](?:\d{2})?(?::\d{2})?(?::\d{2})?(?:.\d+)?(?:[+-]\d{2}(?::?\d{2})?|[Zz])?(?:\[[^\]]+\])?)?$/.test( + value + ) ) { return DataFieldType.Date; } diff --git a/src/main.ts b/src/main.ts index aa87c764..7c75b3f7 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,6 +1,3 @@ -import dayjs from "dayjs"; -import isoWeek from "dayjs/plugin/isoWeek"; -import localizedFormat from "dayjs/plugin/localizedFormat"; import { either, task, taskEither } from "fp-ts"; import { pipe } from "fp-ts/lib/function"; import { Plugin, TFolder, WorkspaceLeaf, addIcon } from "obsidian"; @@ -25,9 +22,6 @@ import { } from "./settings/settings"; import { ProjectsView, VIEW_TYPE_PROJECTS } from "./view"; -dayjs.extend(isoWeek); -dayjs.extend(localizedFormat); - const PROJECTS_PLUGIN_ID = "obsidian-projects"; export default class ProjectsPlugin extends Plugin { diff --git a/src/ui/app/filterFunctions.ts b/src/ui/app/filterFunctions.ts index 993ac7ad..ee5288f4 100644 --- a/src/ui/app/filterFunctions.ts +++ b/src/ui/app/filterFunctions.ts @@ -1,5 +1,4 @@ import { produce } from "immer"; -import dayjs from "dayjs"; import { type DataFrame, type DataRecord, @@ -27,6 +26,7 @@ import { type DateFilterOperator, type ListFilterOperator, } from "src/settings/settings"; +import { Temporal } from "temporal-polyfill"; export function matchesCondition( cond: FilterCondition, @@ -61,10 +61,30 @@ export function matchesCondition( } else if (isOptionalBoolean(value) && isBooleanFilterOperator(operator)) { return booleanFns[operator](value); } else if (isOptionalDate(value) && isDateFilterOperator(operator)) { - return dateFns[operator]( - value, - cond.value ? dayjs(cond.value).toDate() : undefined - ); + let parsedValue = null; // TODO: extract to help functions / processing numbers + if (cond.value) { + try { + // Attempt to parse as ZonedDateTime + parsedValue = Temporal.ZonedDateTime.from(cond.value); + } catch { + try { + // Attempt to parse as Instant and convert to ZonedDateTime + parsedValue = Temporal.Instant.from(cond.value).toZonedDateTimeISO( + cond.value + ); + } catch { + try { + // Attempt to create ZonedDateTime using the current time and a PlainDate + parsedValue = Temporal.PlainDateTime.from( + cond.value + ).toZonedDateTime(Temporal.Now.timeZoneId()); + } catch { + parsedValue = null; // Default to null if all parsing fails + } + } + } + } + return dateFns[operator](value, parsedValue ?? undefined); } return false; @@ -137,22 +157,28 @@ export const booleanFns: Record< }; export const dateFns: Record< + //TODO: handle datetime DateFilterOperator, - (left: Optional, right?: Optional) => boolean + ( + left: Optional, + right?: Optional + ) => boolean > = { "is-on": (left, right) => { - return left && right ? left.getTime() == right.getTime() : false; + return left && right ? left.equals(right) : false; }, "is-not-on": (left, right) => - left && right ? left.getTime() != right.getTime() : true, + left?.toPlainDateTime() && right?.toPlainDateTime() + ? !left.toPlainDateTime().equals(right.toPlainDateTime()) + : true, "is-before": (left, right) => - left && right ? left.getTime() < right.getTime() : false, + left && right ? Temporal.PlainDateTime.compare(left, right) == -1 : false, "is-after": (left, right) => - left && right ? left.getTime() > right.getTime() : false, + left && right ? Temporal.PlainDateTime.compare(left, right) == 1 : false, "is-on-and-before": (left, right) => - left && right ? left.getTime() <= right.getTime() : false, + left && right ? Temporal.PlainDateTime.compare(left, right) < 1 : false, "is-on-and-after": (left, right) => - left && right ? left.getTime() >= right.getTime() : false, + left && right ? Temporal.PlainDateTime.compare(left, right) > -1 : false, }; export const listFns_multitext: Record< diff --git a/src/ui/app/onboarding/demoProject.ts b/src/ui/app/onboarding/demoProject.ts index 3856b296..0cae62ea 100644 --- a/src/ui/app/onboarding/demoProject.ts +++ b/src/ui/app/onboarding/demoProject.ts @@ -1,6 +1,6 @@ -import dayjs from "dayjs"; import { normalizePath, stringifyYaml, type Vault } from "obsidian"; import { v4 as uuidv4 } from "uuid"; +import { Temporal } from "temporal-polyfill"; import { settings } from "src/lib/stores/settings"; import type { BoardConfig } from "src/ui/views/Board/types"; @@ -14,12 +14,12 @@ export async function createDemoProject(vault: Vault) { await vault.createFolder(demoFolder); - const startDate = dayjs(); + const startDate = Temporal.Now.zonedDateTimeISO(); const files = { "The Best Notes You'll Ever Make": { status: "Done", - due: startDate.subtract(2, "weeks").format("YYYY-MM-DD"), + due: startDate.subtract({weeks: 2}).toPlainDate().toString(), published: true, weight: 1, tags: ["note-taking"], @@ -28,7 +28,7 @@ export async function createDemoProject(vault: Vault) { }, "The Easiest Way to Start Taking Notes": { status: "Done", - due: startDate.subtract(1, "weeks").format("YYYY-MM-DD"), + due: startDate.subtract({weeks: 1}).toPlainDate().toString(), published: true, weight: 2, tags: ["note-taking", "obsidian"], @@ -37,7 +37,7 @@ export async function createDemoProject(vault: Vault) { }, "Why You Should Be Taking More Notes": { status: "Doing", - due: startDate.format("YYYY-MM-DD"), + due: startDate.toPlainDate().toString(), published: false, weight: 3, tags: ["note-taking", "pkm"], @@ -46,7 +46,7 @@ export async function createDemoProject(vault: Vault) { }, "What I Learned From Taking 15,000 Notes": { status: "Backlog", - due: startDate.add(1, "weeks").format("YYYY-MM-DD"), + due: startDate.add({weeks: 1}).toPlainDate().toString(), published: false, weight: 4, tags: ["pkm", "obsidian"], @@ -55,7 +55,7 @@ export async function createDemoProject(vault: Vault) { }, "5 Mistakes I Made When I Started Using Obsidian": { status: "Backlog", - due: startDate.add(2, "weeks").format("YYYY-MM-DD"), + due: startDate.add({weeks: 2}).toPlainDate().toString(), published: false, tags: ["obsidian"], image: diff --git a/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte b/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte index 874fa346..f69ae3c2 100644 --- a/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte +++ b/src/ui/app/toolbar/viewOptions/color/ColorOptions.svelte @@ -1,6 +1,6 @@ -{#if field.type === DataFieldType.Date && !field.repeated && value instanceof Date} - {Intl.DateTimeFormat("default", { +{#if field.type === DataFieldType.Date && !field.repeated && isDate(value)} + {value.toPlainDateTime().toLocaleString("default", { year: "numeric", month: "numeric", day: "numeric", - }).format(value)} + })} {/if} diff --git a/src/ui/components/CardMetadata/Datetime.svelte b/src/ui/components/CardMetadata/Datetime.svelte index ed272094..04cdf005 100644 --- a/src/ui/components/CardMetadata/Datetime.svelte +++ b/src/ui/components/CardMetadata/Datetime.svelte @@ -1,6 +1,7 @@ -{#if field.type === DataFieldType.Date && !field.repeated && value instanceof Date} - {Intl.DateTimeFormat("default", { +{#if field.type === DataFieldType.Date && !field.repeated && isDate(value)} + {value.toPlainDateTime().toLocaleString("default", { year: "numeric", month: "numeric", day: "numeric", hour: "numeric", minute: "numeric", hour12: false, - }).format(value)} + })} {/if} diff --git a/src/ui/components/DateInput.svelte b/src/ui/components/DateInput.svelte index e7ca4a42..3410eecf 100644 --- a/src/ui/components/DateInput.svelte +++ b/src/ui/components/DateInput.svelte @@ -1,11 +1,11 @@