From 67fa1d6bb43db2bb083b07114e7f4502c73b5c1e Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Wed, 13 Mar 2024 08:59:23 +0500 Subject: [PATCH] core: sort & group reminders in sqlite --- packages/core/__benches__/reminders.bench.ts | 186 ++++++++++++++++ packages/core/__tests__/reminders.test.js | 201 ++++++++++++++---- packages/core/src/collections/reminders.ts | 59 ++++- packages/core/src/collections/settings.ts | 2 +- packages/core/src/database/index.ts | 2 - .../src/database/sql-cached-collection.ts | 6 +- packages/core/src/database/sql-collection.ts | 70 +++--- packages/core/src/types.ts | 1 - 8 files changed, 431 insertions(+), 96 deletions(-) create mode 100644 packages/core/__benches__/reminders.bench.ts diff --git a/packages/core/__benches__/reminders.bench.ts b/packages/core/__benches__/reminders.bench.ts new file mode 100644 index 000000000..4e03cc6e7 --- /dev/null +++ b/packages/core/__benches__/reminders.bench.ts @@ -0,0 +1,186 @@ +/* +This file is part of the Notesnook project (https://notesnook.com/) + +Copyright (C) 2023 Streetwriters (Private) Limited + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +*/ + +import { bench, describe } from "vitest"; +import { databaseTest } from "../__tests__/utils"; +import Database from "../src/api"; +import MockDate from "mockdate"; +import dayjs from "dayjs"; +import { Reminder } from "../src/types"; +import { + createUpcomingReminderTimeQuery, + getUpcomingReminderTime +} from "../src/collections/reminders"; + +async function addReminders(db: Database) { + const reminders: Partial[] = [ + { + recurringMode: "day", + date: new Date(0).setHours(14), + mode: "repeat" + }, + { + recurringMode: "day", + date: new Date(0).setHours(3), + mode: "repeat", + title: "Random reminder" + }, + { + recurringMode: "week", + date: new Date(0).setHours(8), + selectedDays: [3, 5], + mode: "repeat" + }, + { + recurringMode: "week", + date: dayjs().hour(3).valueOf(), + selectedDays: [0], + mode: "repeat" + }, + { + recurringMode: "week", + date: new Date(0).setHours(8), + selectedDays: [0, 5, 6], + mode: "repeat" + }, + { + recurringMode: "week", + date: new Date(0).setHours(21), + selectedDays: [0, 1], + mode: "repeat" + }, + { + recurringMode: "week", + date: new Date(5).setHours(21), + selectedDays: [1, 2, 5, 6], + mode: "repeat" + }, + { + recurringMode: "month", + date: new Date(0).setHours(8), + selectedDays: [12, 18], + mode: "repeat" + }, + { + recurringMode: "month", + date: new Date(0).setHours(3), + selectedDays: [1, 2, 3], + mode: "repeat" + }, + { + recurringMode: "month", + date: new Date(0).setHours(21), + selectedDays: [6], + mode: "repeat" + }, + { + recurringMode: "month", + date: new Date(0).setHours(21), + selectedDays: [6, 7, 8], + mode: "repeat" + }, + { + date: new Date(2022, 5, 6, 8, 5).getTime(), + mode: "once" + }, + { + date: new Date(2022, 5, 7, 8, 5).getTime(), + mode: "once" + }, + { + date: new Date(2022, 5, 5, 8, 5).getTime(), + mode: "once" + }, + { + date: new Date(2022, 5, 6, 5, 5).getTime(), + mode: "once" + }, + { + date: new Date(2022, 5, 6, 3, 5).getTime(), + mode: "once" + }, + { + date: new Date(2022, 5, 6, 5, 5).getTime(), + mode: "repeat", + recurringMode: "day" + }, + { + recurringMode: "week", + date: new Date(0).setHours(3), + selectedDays: [1], + mode: "repeat" + }, + { + recurringMode: "year", + date: dayjs().month(7).date(20).hour(5).minute(5).valueOf(), + selectedDays: [], + mode: "repeat" + }, + { + recurringMode: "year", + date: dayjs().month(2).date(20).hour(5).minute(5).valueOf(), + selectedDays: [], + mode: "repeat" + } + ]; + for (let i = 0; i < 10000; ++i) { + const random = reminders[getRandom(0, reminders.length)]; + if (!random) continue; + await db.reminders.add({ + title: "Random reminder", + ...random + }); + if (i % 100 === 0) console.log(i); + } + console.log("DONE"); +} + +describe("reminders", async () => { + MockDate.set(new Date(2022, 5, 6, 5, 5, 0, 0)); + + const db = await databaseTest(); + await addReminders(db); + + bench("derive due date in sqlite", async () => { + await db + .sql() + .selectFrom("reminders") + .select([ + createUpcomingReminderTimeQuery(dayjs().format("YYYY-MM-DDTHH:mm")).as( + "dueDate" + ) + ]) + .execute(); + }); + + bench("derive due date in js", async () => { + const reminders = (await db + .sql() + .selectFrom("reminders") + .select(["type", "date", "mode", "recurringMode", "date", "selectedDays"]) + .execute()) as Reminder[]; + reminders.map((r) => getUpcomingReminderTime(r)); + }); + + MockDate.reset(); +}); + +function getRandom(min: number, max: number) { + return Math.round(Math.random() * (max - min) + min); +} diff --git a/packages/core/__tests__/reminders.test.js b/packages/core/__tests__/reminders.test.js index 1ed4a9f57..5c1c50792 100644 --- a/packages/core/__tests__/reminders.test.js +++ b/packages/core/__tests__/reminders.test.js @@ -17,9 +17,18 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { formatReminderTime } from "../src/collections/reminders"; +import { + createIsReminderActiveQuery, + createUpcomingReminderTimeQuery, + formatReminderTime, + getUpcomingReminderTime, + isReminderActive +} from "../src/collections/reminders"; import MockDate from "mockdate"; import { describe, afterAll, beforeAll, test, expect } from "vitest"; +import { databaseTest } from "./utils"; +import dayjs from "dayjs"; +import assert from "assert"; describe("format reminder time", () => { afterAll(() => { @@ -27,185 +36,297 @@ describe("format reminder time", () => { }); beforeAll(() => { - MockDate.set(new Date(2022, 5, 6, 5, 5)); + MockDate.set(new Date(2022, 5, 6, 5, 5, 0, 0)); }); - test("daily reminder [today]", () => { + test("daily reminder [today]", async () => { const reminder = { recurringMode: "day", date: new Date(0).setHours(14), - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Today, 02:00 PM"); }); - test("daily reminder [tomorrow]", () => { + test("daily reminder [tomorrow]", async () => { const reminder = { recurringMode: "day", date: new Date(0).setHours(3), - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Tomorrow, 03:00 AM"); }); - test("weekly reminder [current week]", () => { + test("weekly reminder [current week]", async () => { const reminder = { recurringMode: "week", date: new Date(0).setHours(8), selectedDays: [3, 5], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe( "Upcoming: Wed, 08-06-2022 08:00 AM" ); }); - test("weekly reminder [next week]", () => { + test("weekly reminder [next week]", async () => { const reminder = { recurringMode: "week", - date: new Date(0).setHours(3), + date: dayjs().hour(3).valueOf(), selectedDays: [0], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe( - "Upcoming: Sun, 12-06-2022 03:00 AM" + "Upcoming: Sun, 12-06-2022 03:05 AM" ); }); - test("weekly reminder [current week, multiple days]", () => { + test("weekly reminder [current week, multiple days]", async () => { const reminder = { recurringMode: "week", date: new Date(0).setHours(8), selectedDays: [0, 5, 6], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe( "Upcoming: Fri, 10-06-2022 08:00 AM" ); }); - test("weekly reminder [current week, today]", () => { + test("weekly reminder [current week, today]", async () => { const reminder = { recurringMode: "week", date: new Date(0).setHours(21), selectedDays: [0, 1], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Today, 09:00 PM"); }); - test("weekly reminder [current week, today with multiple days]", () => { + test("weekly reminder [current week, today with multiple days]", async () => { const reminder = { recurringMode: "week", date: new Date(5).setHours(21), selectedDays: [1, 2, 5, 6], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Today, 09:00 PM"); }); - test("monthly reminder [current month]", () => { + test("monthly reminder [current month]", async () => { const reminder = { recurringMode: "month", date: new Date(0).setHours(8), selectedDays: [12, 18], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe( "Upcoming: Sun, 12-06-2022 08:00 AM" ); }); - test("monthly reminder [next month]", () => { + test("monthly reminder [next month]", async () => { const reminder = { recurringMode: "month", date: new Date(0).setHours(3), selectedDays: [1, 2, 3], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe( "Upcoming: Fri, 01-07-2022 03:00 AM" ); }); - test("monthly reminder [current month, today]", () => { + test("monthly reminder [current month, today]", async () => { const reminder = { recurringMode: "month", date: new Date(0).setHours(21), selectedDays: [6], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Today, 09:00 PM"); }); - test("monthly reminder [current month, today with multiple days]", () => { + test("monthly reminder [current month, today with multiple days]", async () => { const reminder = { recurringMode: "month", date: new Date(0).setHours(21), selectedDays: [6, 7, 8], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Today, 09:00 PM"); }); - test("today", () => { + test("today", async () => { const reminder = { date: new Date(2022, 5, 6, 8, 5).getTime(), - mode: "once" + mode: "once", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Today, 08:05 AM"); }); - test("tomorrow", () => { + test("tomorrow", async () => { const reminder = { date: new Date(2022, 5, 7, 8, 5).getTime(), - mode: "once" + mode: "once", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Tomorrow, 08:05 AM"); }); - test("yesterday", () => { + test("yesterday", async () => { const reminder = { date: new Date(2022, 5, 5, 8, 5).getTime(), - mode: "once" + mode: "once", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Last: Yesterday, 08:05 AM"); }); - test("exactly on time", () => { + test("exactly on time", async () => { const reminder = { date: new Date(2022, 5, 6, 5, 5).getTime(), - mode: "once" + mode: "once", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Last: Today, 05:05 AM"); }); - test("past but still on the same day", () => { + test("past but still on the same day", async () => { const reminder = { date: new Date(2022, 5, 6, 3, 5).getTime(), - mode: "once" + mode: "once", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Last: Today, 03:05 AM"); }); - test("the exact current time tomorrow", () => { + test("the exact current time tomorrow", async () => { const reminder = { date: new Date(2022, 5, 6, 5, 5).getTime(), mode: "repeat", - recurringMode: "day" + recurringMode: "day", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe("Upcoming: Tomorrow, 05:05 AM"); }); - test("same day next week because time has passed today", () => { + test("same day next week because time has passed today", async () => { const reminder = { recurringMode: "week", date: new Date(0).setHours(3), selectedDays: [1], - mode: "repeat" + mode: "repeat", + title: "Random reminder" }; + + expect(await compareReminder(reminder)).toBe(true); expect(formatReminderTime(reminder)).toBe( "Upcoming: Mon, 13-06-2022 03:00 AM" ); }); + + test("yearly reminder [this year]", async () => { + const reminder = { + recurringMode: "year", + date: dayjs().month(7).date(20).hour(5).minute(5).valueOf(), + selectedDays: [], + mode: "repeat", + title: "Random reminder" + }; + + expect(await compareReminder(reminder)).toBe(true); + expect(formatReminderTime(reminder)).toBe( + "Upcoming: Sat, 20-08-2022 05:05 AM" + ); + }); + + test("yearly reminder [next year]", async () => { + const reminder = { + recurringMode: "year", + date: dayjs().month(2).date(20).hour(5).minute(5).valueOf(), + selectedDays: [], + mode: "repeat", + title: "Random reminder" + }; + + expect(await compareReminder(reminder)).toBe(true); + expect(formatReminderTime(reminder)).toBe( + "Upcoming: Mon, 20-03-2023 05:05 AM" + ); + }); }); + +async function compareReminder(reminder) { + const db = await databaseTest(); + const id = await db.reminders.add(reminder); + const result = await db + .sql() + .selectFrom("reminders") + .select([ + createUpcomingReminderTimeQuery(dayjs().format("YYYY-MM-DDTHH:mm")).as( + "dueDate" + ), + createIsReminderActiveQuery(dayjs().format("YYYY-MM-DDTHH:mm")).as( + "isActive" + ), + "id" + ]) + .where("id", "=", id) + .executeTakeFirst(); + + assert( + result.isActive === Number(isReminderActive(reminder)), + "is active value is not equal" + ); + return ( + result.dueDate === + dayjs(getUpcomingReminderTime(reminder)).second(0).millisecond(0).valueOf() + ); +} diff --git a/packages/core/src/collections/reminders.ts b/packages/core/src/collections/reminders.ts index e547112f8..9db421345 100644 --- a/packages/core/src/collections/reminders.ts +++ b/packages/core/src/collections/reminders.ts @@ -29,6 +29,7 @@ import { Reminder } from "../types"; import Database from "../api"; import { SQLCollection } from "../database/sql-collection"; import { isFalse } from "../database"; +import { sql } from "kysely"; dayjs.extend(isTomorrow); dayjs.extend(isSameOrBefore); @@ -66,7 +67,7 @@ export class Reminders implements ICollection { }; if (!reminder.date || !reminder.title) - throw new Error("date and title are required in a reminder."); + throw new Error(`date and title are required in a reminder.`); await this.collection.upsert({ id, @@ -247,15 +248,57 @@ export function getUpcomingReminder(reminders: Reminder[]) { } export function isReminderActive(reminder: Reminder) { - const time = - reminder.mode === "once" - ? reminder.date - : getUpcomingReminderTime(reminder); - return ( !reminder.disabled && (reminder.mode !== "once" || - time > Date.now() || - (reminder.snoozeUntil && reminder.snoozeUntil > Date.now())) + reminder.date > Date.now() || + (!!reminder.snoozeUntil && reminder.snoozeUntil > Date.now())) ); } + +export function createUpcomingReminderTimeQuery(now = "now") { + return sql`CASE + WHEN mode = 'once' THEN date / 1000 + WHEN recurringMode = 'year' THEN + strftime('%s', + strftime('%Y-', date(${now})) || strftime('%m-%d%H:%M', date / 1000, 'unixepoch', 'localtime'), + IIF(datetime(strftime('%Y-', date(${now})) || strftime('%m-%d%H:%M', date / 1000, 'unixepoch', 'localtime')) <= datetime(${now}), '+1 year', '+0 year'), + 'utc' + ) + WHEN recurringMode = 'day' THEN + strftime('%s', + date(${now}) || time(date / 1000, 'unixepoch', 'localtime'), + IIF(datetime(date(${now}) || time(date / 1000, 'unixepoch', 'localtime')) <= datetime(${now}), '+1 day', '+0 day'), + 'utc' + ) + WHEN recurringMode = 'week' AND selectedDays IS NOT NULL AND json_array_length(selectedDays) > 0 THEN + CASE + WHEN CAST(strftime('%w', date(${now})) AS INTEGER) > (SELECT MAX(value) FROM json_each(selectedDays)) + OR datetime(date(${now}) || time(date / 1000, 'unixepoch', 'localtime')) <= datetime(${now}) + THEN + strftime('%s', datetime(date(${now}), time(date / 1000, 'unixepoch', 'localtime'), '+1 day', 'weekday ' || json_extract(selectedDays, '$[0]'), 'utc')) + ELSE + strftime('%s', datetime(date(${now}), time(date / 1000, 'unixepoch', 'localtime'), 'weekday ' || (SELECT value FROM json_each(selectedDays) WHERE CAST(strftime('%w', date(${now})) AS INTEGER) <= value), 'utc')) + END + WHEN recurringMode = 'month' AND selectedDays IS NOT NULL AND json_array_length(selectedDays) > 0 THEN + CASE + WHEN CAST(strftime('%d', date(${now})) AS INTEGER) > (SELECT MAX(value) FROM json_each(selectedDays)) + OR datetime(date(${now}) || time(date / 1000, 'unixepoch', 'localtime')) <= datetime(${now}) + THEN + strftime('%s', strftime('%Y-%m-', date(${now})) || printf('%02d', json_extract(selectedDays, '$[0]')) || time(date / 1000, 'unixepoch', 'localtime'), '+1 month', 'utc') + ELSE strftime('%s', strftime('%Y-%m-', date(${now})) || (SELECT printf('%02d', value) FROM json_each(selectedDays) WHERE value <= strftime('%d', date(${now}))) || time(date / 1000, 'unixepoch', 'localtime'), 'utc') + END + ELSE strftime('%s', date(${now}) || time(date / 1000, 'unixepoch', 'localtime'), 'utc') + END * 1000 +`.$castTo(); +} + +export function createIsReminderActiveQuery(now = "now") { + return sql`IIF( + (disabled IS NULL OR disabled = 0) + AND (mode != 'once' + OR datetime(date / 1000, 'unixepoch', 'localtime') > datetime(${now}) + OR (snoozeUntil IS NOT NULL + AND datetime(snoozeUntil / 1000, 'unixepoch', 'localtime') > datetime(${now})) + ), 1, 0)`.$castTo(); +} diff --git a/packages/core/src/collections/settings.ts b/packages/core/src/collections/settings.ts index 72faf6039..136e83407 100644 --- a/packages/core/src/collections/settings.ts +++ b/packages/core/src/collections/settings.ts @@ -40,7 +40,7 @@ const DEFAULT_GROUP_OPTIONS = (key: GroupingKey) => sortBy: key === "trash" ? "dateDeleted" - : key === "tags" || key === "reminders" + : key === "tags" ? "dateCreated" : key === "reminders" ? "dueDate" diff --git a/packages/core/src/database/index.ts b/packages/core/src/database/index.ts index ce9672814..6dbff40dc 100644 --- a/packages/core/src/database/index.ts +++ b/packages/core/src/database/index.ts @@ -40,7 +40,6 @@ import { Attachment, Color, ContentItem, - GroupOptions, HistorySession, ItemType, MaybeDeletedItem, @@ -125,7 +124,6 @@ export interface DatabaseCollection { get(id: string): AsyncOrSyncResult; put(items: (T | undefined)[]): Promise[]>; update(ids: string[], partial: Partial): Promise; - ids(options: GroupOptions): AsyncOrSyncResult; records( ids: string[] ): AsyncOrSyncResult< diff --git a/packages/core/src/database/sql-cached-collection.ts b/packages/core/src/database/sql-cached-collection.ts index 042dd9a07..0a898cf8c 100644 --- a/packages/core/src/database/sql-cached-collection.ts +++ b/packages/core/src/database/sql-cached-collection.ts @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { GroupOptions, MaybeDeletedItem, isDeleted } from "../types"; +import { MaybeDeletedItem, isDeleted } from "../types"; import EventManager from "../utils/event-manager"; import { DatabaseAccessor, DatabaseCollection, DatabaseSchema } from "."; import { SQLCollection } from "./sql-collection"; @@ -126,10 +126,6 @@ export class SQLCachedCollection< } } - ids(_options: GroupOptions): string[] { - return Array.from(this.cache.keys()); - } - records(ids: string[]): Record | undefined> { const items: Record | undefined> = {}; for (const id of ids) { diff --git a/packages/core/src/database/sql-collection.ts b/packages/core/src/database/sql-collection.ts index 4fe7971af..833084d40 100644 --- a/packages/core/src/database/sql-collection.ts +++ b/packages/core/src/database/sql-collection.ts @@ -34,6 +34,7 @@ import { isFalse } from "."; import { + AliasedRawBuilder, AnyColumn, AnyColumnWithTable, ExpressionOrFactory, @@ -46,6 +47,10 @@ import { VirtualizedGrouping } from "../utils/virtualized-grouping"; import { groupArray } from "../utils/grouping"; import { toChunks } from "../utils/array"; import { Sanitizer } from "./sanitizer"; +import { + createIsReminderActiveQuery, + createUpcomingReminderTimeQuery +} from "../collections/reminders"; const formats = { month: "%Y-%m", @@ -207,19 +212,6 @@ export class SQLCollection< } } - async ids(sortOptions: GroupOptions): Promise { - const ids = await this.db() - .selectFrom(this.type) - .select("id") - .where(isFalse("deleted")) - .$if(this.type === "notes" || this.type === "notebooks", (eb) => - eb.where(isFalse("dateDeleted")) - ) - .orderBy(sortOptions.sortBy, sortOptions.sortDirection) - .execute(); - return ids.map((id) => id.id); - } - async records( ids: string[] ): Promise | undefined>> { @@ -432,26 +424,26 @@ export class FilteredSelector { const fields: Array< | AnyColumnWithTable | AnyColumn - > = ["id", "type", options.sortBy]; + | AliasedRawBuilder + > = ["id", "type"]; if (this.type === "notes") fields.push("notes.pinned", "notes.conflicted"); else if (this.type === "notebooks") fields.push("notebooks.pinned"); else if (this.type === "attachments" && options.groupBy === "abc") fields.push("attachments.filename"); - else if (this.type === "reminders") { + else if (this.type === "reminders" || options.sortBy === "dueDate") { fields.push( "reminders.mode", - "reminders.date", - "reminders.recurringMode", - "reminders.selectedDays", + "reminders.snoozeUntil", "reminders.disabled", - "reminders.snoozeUntil" + "reminders.date", + createUpcomingReminderTimeQuery().as("dueDate") ); - } + } else fields.push(options.sortBy); return Array.from( groupArray( await this.filter - .$call(this.buildSortExpression(options)) .select(fields) + .$call(this.buildSortExpression(options, true)) .execute(), options ).values() @@ -508,7 +500,7 @@ export class FilteredSelector { private buildSortExpression( options: GroupOptions | SortOptions, - persistent?: boolean + hasDueDate?: boolean ) { const sortBy: Set = new Set(); if (isGroupOptions(options)) { @@ -524,6 +516,11 @@ export class FilteredSelector { qb = qb.orderBy(sql`IFNULL(conflicted, 0) desc`); if (this.type === "notes" || this.type === "notebooks") qb = qb.orderBy(sql`IFNULL(pinned, 0) desc`); + if (this.type === "reminders") + qb = qb.orderBy( + (qb) => qb.parens(createIsReminderActiveQuery()), + "desc" + ); for (const item of sortBy) { if (item === "title") { @@ -538,36 +535,30 @@ export class FilteredSelector { ? formats[options.groupBy] : null; if (!timeFormat || isSortByDate(options)) { - qb = qb.orderBy(item, options.sortDirection); + if (item === "dueDate") { + if (hasDueDate) + qb = qb.orderBy(item as any, options.sortDirection); + else + qb = qb.orderBy( + (qb) => qb.parens(createUpcomingReminderTimeQuery()), + options.sortDirection + ); + } else qb = qb.orderBy(item, options.sortDirection); continue; } qb = qb.orderBy( sql`strftime('${sql.raw(timeFormat)}', ${sql.raw( item - )} / 1000, 'unixepoch')`, + )} / 1000, 'unixepoch', 'localtime')`, options.sortDirection ); } } - if (persistent) qb = qb.orderBy("id asc"); return qb; }; } - - private sortFields(options: SortOptions, persistent?: boolean) { - const fields: Array< - | AnyColumnWithTable - | AnyColumn - > = []; - if (this.type === "notes") fields.push("conflicted"); - if (this.type === "notes" || this.type === "notebooks") - fields.push("pinned"); - fields.push(options.sortBy); - if (persistent) fields.push("id"); - return fields; - } } function isGroupOptions( @@ -582,6 +573,7 @@ function isSortByDate(options: SortOptions | GroupOptions) { options.sortBy === "dateEdited" || options.sortBy === "dateDeleted" || options.sortBy === "dateModified" || - options.sortBy === "dateUploaded" + options.sortBy === "dateUploaded" || + options.sortBy === "dueDate" ); } diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 76bb7e0e3..926e4113f 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -45,7 +45,6 @@ export type GroupingKey = | "notes" | "notebooks" | "tags" - //| "topics" | "trash" | "favorites" | "reminders";