diff --git a/apps/web/src/common/db.ts b/apps/web/src/common/db.ts
index 1ecf6d8c9..079986624 100644
--- a/apps/web/src/common/db.ts
+++ b/apps/web/src/common/db.ts
@@ -20,25 +20,21 @@ along with this program. If not, see .
import { EventSourcePolyfill as EventSource } from "event-source-polyfill";
import { DatabasePersistence, NNStorage } from "../interfaces/storage";
import { logger } from "../utils/logger";
-import type Database from "@notesnook/core/dist/api";
import { showMigrationDialog } from "./dialog-controller";
// import { SQLocalKysely } from "sqlocal/kysely";
import { WaSqliteWorkerDriver } from "./sqlite/sqlite.kysely";
import { SqliteAdapter, SqliteQueryCompiler, SqliteIntrospector } from "kysely";
+import { database } from "@notesnook/common";
// import SQLiteESMFactory from "./sqlite/wa-sqlite-async";
// import * as SQLite from "./sqlite/sqlite-api";
// import { IDBBatchAtomicVFS } from "./sqlite/IDBBatchAtomicVFS";
-// eslint-disable-next-line @typescript-eslint/ban-ts-comment
-// @ts-ignore
-let db: Database = {};
+const db = database;
async function initializeDatabase(persistence: DatabasePersistence) {
logger.measure("Database initialization");
- const { database } = await import("@notesnook/common");
const { FileStorage } = await import("../interfaces/fs");
const { Compressor } = await import("../utils/compressor");
- db = database;
// // const ss = wrap(new Worker());
// // await ss.init("test.db", false, uri);
diff --git a/apps/web/src/components/navigation-menu/index.tsx b/apps/web/src/components/navigation-menu/index.tsx
index e6c0a0485..6e056179c 100644
--- a/apps/web/src/components/navigation-menu/index.tsx
+++ b/apps/web/src/components/navigation-menu/index.tsx
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { useCallback, useEffect } from "react";
+import { useCallback, useEffect, useState } from "react";
import { Box, Button, Flex } from "@theme-ui/components";
import {
Note,
@@ -38,7 +38,7 @@ import {
Reminders
} from "../icons";
import { AnimatedFlex } from "../animated";
-import NavigationItem from "./navigation-item";
+import NavigationItem, { SortableNavigationItem } from "./navigation-item";
import { hardNavigate, hashNavigate, navigate } from "../../navigation";
import { db } from "../../common/db";
import useMobile from "../../hooks/use-mobile";
@@ -49,8 +49,28 @@ import { useStore as useThemeStore } from "../../stores/theme-store";
import useLocation from "../../hooks/use-location";
import { FlexScrollContainer } from "../scroll-container";
import { ScopedThemeProvider } from "../theme-provider";
+import {
+ closestCenter,
+ DndContext,
+ useSensor,
+ useSensors,
+ KeyboardSensor,
+ DragOverlay,
+ MeasuringStrategy,
+ MouseSensor
+} from "@dnd-kit/core";
+import {
+ arrayMove,
+ SortableContext,
+ sortableKeyboardCoordinates,
+ verticalListSortingStrategy
+} from "@dnd-kit/sortable";
+import { isUserPremium } from "../../hooks/use-is-user-premium";
+import { showToast } from "../../utils/toast";
+import { usePersistentState } from "../../hooks/use-persistent-state";
type Route = {
+ id: string;
title: string;
path: string;
icon: Icon;
@@ -68,33 +88,38 @@ function shouldSelectNavItem(route: string, pin: { type: string; id: string }) {
}
const routes: Route[] = [
- { title: "Notes", path: "/notes", icon: Note },
+ { id: "notes", title: "Notes", path: "/notes", icon: Note },
{
+ id: "notebooks",
title: "Notebooks",
path: "/notebooks",
icon: Notebook
},
{
+ id: "favorites",
title: "Favorites",
path: "/favorites",
icon: StarOutline
},
- { title: "Tags", path: "/tags", icon: Tag },
- {
- title: "Monographs",
- path: "/monographs",
- icon: Monographs
- },
+ { id: "tags", title: "Tags", path: "/tags", icon: Tag },
{
+ id: "reminders",
title: "Reminders",
path: "/reminders",
icon: Reminders,
tag: "Beta"
},
- { title: "Trash", path: "/trash", icon: Trash }
+ {
+ id: "monographs",
+ title: "Monographs",
+ path: "/monographs",
+ icon: Monographs
+ },
+ { id: "trash", title: "Trash", path: "/trash", icon: Trash }
];
const settings: Route = {
+ id: "settings",
title: "Settings",
path: "/settings",
icon: Settings
@@ -180,97 +205,163 @@ function NavigationMenu(props: NavigationMenuProps) {
suppressScrollX={true}
>
- {routes.map((item) => (
- {
- if (!isMobile && location === item.path)
- return toggleNavigationContainer();
- _navigate(item.path);
- }}
- />
- ))}
- {colors.map((color, index) => (
- {
- _navigate(`/colors/${color.id}`);
- }}
- menuItems={[
- {
- type: "button",
- key: "rename",
- title: "Rename color",
- onClick: async () => {
- await showRenameColorDialog(color.id);
- }
+ db.settings.getSideBarOrder("builtin")}
+ onOrderChanged={(order) =>
+ db.settings.setSideBarOrder("builtin", order)
+ }
+ renderOverlay={({ item }) => (
+
- ))}
+ />
+ )}
+ renderItem={({ item }) => (
+ {
+ if (!isMobile && location === item.path)
+ return toggleNavigationContainer();
+ _navigate(item.path);
+ }}
+ />
+ )}
+ />
+
+ db.settings.getSideBarOrder("colors")}
+ onOrderChanged={(order) =>
+ db.settings.setSideBarOrder("colors", order)
+ }
+ renderOverlay={({ item }) => (
+
+ )}
+ renderItem={({ item: color }) => (
+ {
+ _navigate(`/colors/${color.id}`);
+ }}
+ menuItems={[
+ {
+ type: "button",
+ key: "rename",
+ title: "Rename color",
+ onClick: async () => {
+ await showRenameColorDialog(color.id);
+ }
+ }
+ ]}
+ />
+ )}
+ />
- {shortcuts.map((item, index) => (
- {
- await db.shortcuts.remove(item.id);
- refreshNavItems();
+ db.settings.getSideBarOrder("shortcuts")}
+ onOrderChanged={(order) =>
+ db.settings.setSideBarOrder("shortcuts", order)
+ }
+ renderOverlay={({ item }) => (
+
+ )}
+ renderItem={({ item }) => (
+ {
+ await db.shortcuts.remove(item.id);
+ refreshNavItems();
+ }
}
+ ]}
+ icon={
+ item.type === "notebook"
+ ? Notebook2
+ : item.type === "tag"
+ ? Tag2
+ : Topic
}
- ]}
- icon={
- item.type === "notebook"
- ? Notebook2
- : item.type === "tag"
- ? Tag2
- : Topic
- }
- isShortcut
- selected={shouldSelectNavItem(location, item)}
- onClick={() => {
- if (item.type === "notebook") {
- _navigate(`/notebooks/${item.id}`);
- } else if (item.type === "tag") {
- _navigate(`/tags/${item.id}`);
- }
- }}
- />
- ))}
+ isShortcut
+ selected={shouldSelectNavItem(location, item)}
+ onClick={() => {
+ if (item.type === "notebook") {
+ _navigate(`/notebooks/${item.id}`);
+ } else if (item.type === "tag") {
+ _navigate(`/tags/${item.id}`);
+ }
+ }}
+ />
+ )}
+ />
{isLoggedIn === false && (
)}
= {
+ orderKey: string;
+ items: T[];
+ renderItem: (props: { item: T }) => JSX.Element;
+ renderOverlay: (props: { item: T }) => JSX.Element;
+ onOrderChanged: (newOrder: string[]) => void;
+ order: () => string[];
+};
+
+function ReorderableList(
+ props: ReorderableListProps
+) {
+ const {
+ orderKey,
+ items,
+ renderItem: Item,
+ renderOverlay: Overlay,
+ onOrderChanged,
+ order: _order
+ } = props;
+ const sensors = useSensors(
+ useSensor(MouseSensor, {
+ activationConstraint: {
+ distance: 10
+ }
+ }),
+ useSensor(KeyboardSensor, {
+ coordinateGetter: sortableKeyboardCoordinates
+ })
+ );
+ const [activeItem, setActiveItem] = useState();
+ const [order, setOrder] = usePersistentState(orderKey, _order());
+ const orderedItems = orderItems(items, order);
+
+ return (
+ {
+ if (!isUserPremium()) {
+ showToast("error", "You need to be Pro to customize the sidebar.");
+ return;
+ }
+ setActiveItem(orderedItems.find((i) => i.id === event.active.id));
+ }}
+ onDragEnd={(event) => {
+ const { active, over } = event;
+
+ const overId = over?.id as string;
+ if (overId && active.id !== overId) {
+ const transitionOrder =
+ order.length === 0 ? orderedItems.map((i) => i.id) : order;
+ const newIndex = transitionOrder.indexOf(overId);
+ const oldIndex = transitionOrder.indexOf(active.id as string);
+ const newOrder = arrayMove(transitionOrder, oldIndex, newIndex);
+ setOrder(newOrder);
+ onOrderChanged(newOrder);
+ }
+ setActiveItem(undefined);
+ }}
+ measuring={{
+ droppable: { strategy: MeasuringStrategy.Always }
+ }}
+ >
+
+ {orderedItems.map((item) => (
+
+ ))}
+
+
+ {activeItem && }
+
+
+
+ );
+}
+
+function orderItems(items: T[], order: string[]) {
+ const sorted: T[] = [];
+ order.forEach((id) => {
+ const item = items.find((i) => i.id === id);
+ if (!item) return;
+ sorted.push(item);
+ });
+ sorted.push(...items.filter((i) => !order.includes(i.id)));
+ return sorted;
+}
diff --git a/apps/web/src/components/navigation-menu/navigation-item.tsx b/apps/web/src/components/navigation-menu/navigation-item.tsx
index 95f9e37eb..22022c9b7 100644
--- a/apps/web/src/components/navigation-menu/navigation-item.tsx
+++ b/apps/web/src/components/navigation-menu/navigation-item.tsx
@@ -17,15 +17,16 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
*/
-import { Button, Text } from "@theme-ui/components";
+import { Button, Flex, FlexProps, Text } from "@theme-ui/components";
import { useStore as useAppStore } from "../../stores/app-store";
import { Menu } from "../../hooks/use-menu";
import useMobile from "../../hooks/use-mobile";
import { PropsWithChildren } from "react";
import { Icon, Shortcut } from "../icons";
-import { AnimatedFlex } from "../animated";
import { SchemeColors, createButtonVariant } from "@notesnook/theme";
import { MenuItem } from "@notesnook/ui";
+import { useSortable } from "@dnd-kit/sortable";
+import { CSS } from "@dnd-kit/utilities";
type NavigationItemProps = {
icon: Icon;
@@ -43,7 +44,11 @@ type NavigationItemProps = {
menuItems?: MenuItem[];
};
-function NavigationItem(props: PropsWithChildren) {
+function NavigationItem(
+ props: PropsWithChildren<
+ NavigationItemProps & { containerRef?: React.Ref } & FlexProps
+ >
+) {
const {
icon: Icon,
color,
@@ -57,24 +62,17 @@ function NavigationItem(props: PropsWithChildren) {
onClick,
menuItems,
count,
- animate = false,
- index = 0
+ sx,
+ containerRef,
+ ...restProps
} = props;
const toggleSideMenu = useAppStore((store) => store.toggleSideMenu);
const isMobile = useMobile();
return (
- ) {
alignItems: "center",
position: "relative",
":first-of-type": { mt: 1 },
- ":last-of-type": { mb: 1 }
+ ":last-of-type": { mb: 1 },
+ ...sx
// ":hover:not(:disabled)": {
// bg: "hover",
// filter: "brightness(100%)"
@@ -185,7 +184,29 @@ function NavigationItem(props: PropsWithChildren) {
{tag}
) : null}
-
+
);
}
export default NavigationItem;
+
+export function SortableNavigationItem(
+ props: PropsWithChildren<{ id: string } & NavigationItemProps>
+) {
+ const { id, ...restProps } = props;
+ const { attributes, listeners, setNodeRef, transform, transition, active } =
+ useSortable({ id });
+
+ return (
+
+ );
+}
diff --git a/packages/core/src/collections/settings.ts b/packages/core/src/collections/settings.ts
index 9368b02a0..fc022ff4a 100644
--- a/packages/core/src/collections/settings.ts
+++ b/packages/core/src/collections/settings.ts
@@ -24,7 +24,8 @@ import {
GroupingKey,
SettingItem,
SettingItemMap,
- SideBarSectionKey,
+ SideBarHideableSection,
+ SideBarSection,
ToolbarConfig,
ToolbarConfigPlatforms,
TrashCleanupInterval
@@ -63,13 +64,12 @@ const defaultSettings: SettingItemMap = {
"toolbarConfig:desktop": undefined,
"toolbarConfig:mobile": undefined,
+ "sideBarOrder:builtin": [],
"sideBarOrder:colors": [],
- "sideBarOrder:menu": [],
- "sideBarOrder:pinned": [],
+ "sideBarOrder:shortcuts": [],
- "sideBarHiddenItems:colors": [],
- "sideBarHiddenItems:menu": [],
- "sideBarHiddenItems:pinned": []
+ "sideBarHiddenItems:builtin": [],
+ "sideBarHiddenItems:colors": []
};
export class Settings implements ICollection {
@@ -176,19 +176,19 @@ export class Settings implements ICollection {
return this.set("timeFormat", format);
}
- getSideBarOrder(section: SideBarSectionKey) {
+ getSideBarOrder(section: SideBarSection) {
return this.get(`sideBarOrder:${section}`);
}
- setSideBarOrder(section: SideBarSectionKey, order: string[]) {
+ setSideBarOrder(section: SideBarSection, order: string[]) {
return this.set(`sideBarOrder:${section}`, order);
}
- getSideBarHiddenItems(section: SideBarSectionKey) {
+ getSideBarHiddenItems(section: SideBarHideableSection) {
return this.get(`sideBarHiddenItems:${section}`);
}
- setSideBarHiddenItems(section: SideBarSectionKey, order: string[]) {
+ setSideBarHiddenItems(section: SideBarHideableSection, order: string[]) {
return this.set(`sideBarHiddenItems:${section}`, order);
}
}
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index d5d0e21cb..405dfad54 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -33,8 +33,6 @@ export type SortOptions = {
sortDirection: "desc" | "asc";
};
-export type SideBarSectionKey = "menu" | "colors" | "pinned";
-
export type GroupOptions = SortOptions & {
groupBy: "none" | "abc" | "year" | "month" | "week" | "default";
};
@@ -417,6 +415,8 @@ export interface LegacySettingsItem extends BaseItem<"settings"> {
}
export type ToolbarConfigPlatforms = "desktop" | "mobile";
+export type SideBarSection = "builtin" | "colors" | "shortcuts";
+export type SideBarHideableSection = "builtin" | "colors";
export type SettingItemMap = {
trashCleanupInterval: TrashCleanupInterval;
titleFormat: string;
@@ -425,8 +425,8 @@ export type SettingItemMap = {
defaultNotebook: string | undefined;
} & Record<`groupOptions:${GroupingKey}`, GroupOptions> &
Record<`toolbarConfig:${ToolbarConfigPlatforms}`, ToolbarConfig | undefined> &
- Record<`sideBarOrder:${SideBarSectionKey}`, string[]> &
- Record<`sideBarHiddenItems:${SideBarSectionKey}`, string[]>;
+ Record<`sideBarOrder:${SideBarSection}`, string[]> &
+ Record<`sideBarHiddenItems:${SideBarHideableSection}`, string[]>;
export interface SettingItem<
TKey extends keyof SettingItemMap = keyof SettingItemMap