mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
web: new sidebar ui
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com>
This commit is contained in:
@@ -151,6 +151,11 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
const isTablet = useTablet();
|
||||
const [isNarrow, setIsNarrow] = useState(isTablet || false);
|
||||
const navPane = useRef<SplitPaneImperativeHandle>(null);
|
||||
const [isSideMenuOpen, setIsSideMenuOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
AppEventManager.subscribe(AppEvents.toggleSideMenu, setIsSideMenuOpen);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isTablet) navPane.current?.collapse(0);
|
||||
@@ -165,6 +170,40 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
overflow: "hidden"
|
||||
}}
|
||||
>
|
||||
{isTablet && (
|
||||
<Flex
|
||||
sx={{
|
||||
position: "absolute",
|
||||
left: 0,
|
||||
right: 0,
|
||||
zIndex: 1000,
|
||||
width: isSideMenuOpen ? "100%" : 0,
|
||||
height: "100%",
|
||||
background: "transparent",
|
||||
transition: "0.15s width ease-out"
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
top: 0,
|
||||
left: 0,
|
||||
background: "rgba(0,0,0,0.5)"
|
||||
}}
|
||||
onClick={() => {
|
||||
AppEventManager.publish(AppEvents.toggleSideMenu);
|
||||
}}
|
||||
/>
|
||||
<Flex sx={{ width: 300 }}>
|
||||
<NavigationMenu
|
||||
toggleNavigationContainer={() => {}}
|
||||
isTablet={false}
|
||||
/>
|
||||
</Flex>
|
||||
</Flex>
|
||||
)}
|
||||
<SplitPane
|
||||
className="global-split-pane"
|
||||
ref={navPane}
|
||||
@@ -177,17 +216,17 @@ function DesktopAppContents({ show, setShow }: DesktopAppContentsProps) {
|
||||
{isFocusMode ? null : (
|
||||
<Pane
|
||||
id="nav-pane"
|
||||
initialSize={180}
|
||||
initialSize={isTablet ? 0 : 250}
|
||||
className={`nav-pane`}
|
||||
minSize={50}
|
||||
snapSize={120}
|
||||
maxSize={300}
|
||||
minSize={isTablet ? 0 : 200}
|
||||
// snapSize={200}
|
||||
maxSize={isTablet ? 0 : 300}
|
||||
>
|
||||
<NavigationMenu
|
||||
toggleNavigationContainer={(state) => {
|
||||
setShow(state || !show);
|
||||
}}
|
||||
isTablet={isNarrow}
|
||||
isTablet={false}
|
||||
/>
|
||||
</Pane>
|
||||
)}
|
||||
|
||||
@@ -17,8 +17,8 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Box, Button, Flex } from "@theme-ui/components";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { Box, Button, Flex, Image, Text } from "@theme-ui/components";
|
||||
import {
|
||||
Note,
|
||||
Notebook as NotebookIcon,
|
||||
@@ -36,7 +36,11 @@ import {
|
||||
Circle,
|
||||
Icon,
|
||||
Reminders,
|
||||
User
|
||||
User,
|
||||
Home,
|
||||
Pro,
|
||||
Documentation,
|
||||
Logout
|
||||
} from "../icons";
|
||||
import NavigationItem, { SortableNavigationItem } from "./navigation-item";
|
||||
import { hardNavigate, hashNavigate, navigate } from "../../navigation";
|
||||
@@ -72,6 +76,15 @@ import { handleDrop } from "../../common/drop-handler";
|
||||
import { Menu } from "../../hooks/use-menu";
|
||||
import { RenameColorDialog } from "../../dialogs/item-dialog";
|
||||
import { strings } from "@notesnook/intl";
|
||||
import Tags from "../../views/tags";
|
||||
import NotebookTree from "./notebook-tree";
|
||||
import { UserProfile } from "../../dialogs/settings/components/user-profile";
|
||||
import { SUBSCRIPTION_STATUS } from "../../common/constants";
|
||||
import { ConfirmDialog, showLogoutConfirmation } from "../../dialogs/confirm";
|
||||
import { CREATE_BUTTON_MAP, createBackup } from "../../common";
|
||||
import { TaskManager } from "../../common/task-manager";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { useStore } from "../../stores/note-store";
|
||||
|
||||
type Route = {
|
||||
id: string;
|
||||
@@ -79,28 +92,21 @@ type Route = {
|
||||
path: string;
|
||||
icon: Icon;
|
||||
tag?: string;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
const navigationHistory = new Map();
|
||||
function shouldSelectNavItem(route: string, pin: Notebook | Tag) {
|
||||
return route.endsWith(pin.id);
|
||||
}
|
||||
|
||||
const routes: Route[] = [
|
||||
const routesInit: Route[] = [
|
||||
{ id: "notes", title: strings.routes.Notes(), path: "/notes", icon: Note },
|
||||
{
|
||||
id: "notebooks",
|
||||
title: strings.routes.Notebooks(),
|
||||
path: "/notebooks",
|
||||
icon: NotebookIcon
|
||||
},
|
||||
{
|
||||
id: "favorites",
|
||||
title: strings.routes.Favorites(),
|
||||
path: "/favorites",
|
||||
icon: StarOutline
|
||||
},
|
||||
{ id: "tags", title: strings.routes.Tags(), path: "/tags", icon: TagIcon },
|
||||
{
|
||||
id: "reminders",
|
||||
title: strings.routes.Reminders(),
|
||||
@@ -116,12 +122,26 @@ const routes: Route[] = [
|
||||
{ id: "trash", title: strings.routes.Trash(), path: "/trash", icon: Trash }
|
||||
];
|
||||
|
||||
const settings: Route = {
|
||||
const tabs = [
|
||||
{
|
||||
id: "home",
|
||||
icon: Home,
|
||||
title: strings.routes.Home()
|
||||
},
|
||||
{
|
||||
id: "notebook",
|
||||
icon: NotebookIcon,
|
||||
title: strings.routes.Notebooks()
|
||||
},
|
||||
{ id: "tag", icon: TagIcon, title: strings.routes.Tags() }
|
||||
] as const;
|
||||
|
||||
const settings = {
|
||||
id: "settings",
|
||||
title: strings.routes.Settings(),
|
||||
path: "/settings",
|
||||
icon: Settings
|
||||
};
|
||||
} as const;
|
||||
|
||||
type NavigationMenuProps = {
|
||||
toggleNavigationContainer: (toggleState?: boolean) => void;
|
||||
@@ -130,19 +150,13 @@ type NavigationMenuProps = {
|
||||
|
||||
function NavigationMenu(props: NavigationMenuProps) {
|
||||
const { toggleNavigationContainer, isTablet } = props;
|
||||
const [location, previousLocation, state] = useLocation();
|
||||
const [routes, setRoutes] = useState(routesInit);
|
||||
const [location] = useLocation();
|
||||
const isFocusMode = useAppStore((store) => store.isFocusMode);
|
||||
const colors = useAppStore((store) => store.colors);
|
||||
const shortcuts = useAppStore((store) => store.shortcuts);
|
||||
const refreshNavItems = useAppStore((store) => store.refreshNavItems);
|
||||
const isLoggedIn = useUserStore((store) => store.isLoggedIn);
|
||||
const profile = useSettingStore((store) => store.profile);
|
||||
const isMobile = useMobile();
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
|
||||
const setFollowSystemTheme = useThemeStore(
|
||||
(store) => store.setFollowSystemTheme
|
||||
);
|
||||
const [hiddenRoutes, setHiddenRoutes] = usePersistentState(
|
||||
"sidebarHiddenItems:routes",
|
||||
db.settings.getSideBarHiddenItems("routes")
|
||||
@@ -151,26 +165,52 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
"sidebarHiddenItems:colors",
|
||||
db.settings.getSideBarHiddenItems("colors")
|
||||
);
|
||||
const [currentTab, setCurrentTab] = useState<(typeof tabs)[number]["id"]>(
|
||||
tabs.find((tab) => location.includes(tab.id))?.id || "home"
|
||||
);
|
||||
const notes = useStore((store) => store.notes);
|
||||
|
||||
useEffect(() => {
|
||||
const setCounts = async () => {
|
||||
const totalNotes = await db.notes.all.count();
|
||||
const totalFavorites = await db.notes.favorites.count();
|
||||
const totalReminders = await db.reminders.all.count();
|
||||
const totalTrash = (await db.trash.all()).length;
|
||||
const totalMonographs = await db.monographs.all.count();
|
||||
|
||||
setRoutes((routes) => {
|
||||
return routes.map((route) => {
|
||||
switch (route.id) {
|
||||
case "notes":
|
||||
return { ...route, count: totalNotes };
|
||||
case "favorites":
|
||||
return { ...route, count: totalFavorites };
|
||||
case "reminders":
|
||||
return { ...route, count: totalReminders };
|
||||
case "trash":
|
||||
return { ...route, count: totalTrash };
|
||||
case "monographs":
|
||||
return { ...route, count: totalMonographs };
|
||||
default:
|
||||
return route;
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
setCounts();
|
||||
}, [notes]);
|
||||
|
||||
const dragTimeout = useRef(0);
|
||||
|
||||
const _navigate = useCallback(
|
||||
(path: string) => {
|
||||
toggleNavigationContainer(true);
|
||||
const nestedRoute = findNestedRoute(path);
|
||||
navigate(!nestedRoute || nestedRoute === location ? path : nestedRoute);
|
||||
navigate(path);
|
||||
},
|
||||
[location, toggleNavigationContainer]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (state === "forward" || state === "neutral")
|
||||
navigationHistory.set(location, true);
|
||||
else if (state === "same" && location !== previousLocation) {
|
||||
navigationHistory.delete(previousLocation);
|
||||
navigationHistory.set(location, true);
|
||||
} else navigationHistory.delete(previousLocation);
|
||||
}, [location, previousLocation, state]);
|
||||
|
||||
const getSidebarItems = useCallback(async () => {
|
||||
return [
|
||||
{
|
||||
@@ -226,6 +266,69 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
borderRight: "1px solid var(--separator)"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: isTablet ? "column" : "row",
|
||||
alignItems: "center",
|
||||
justifyContent: isTablet ? "center" : "space-between"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: isTablet ? "column" : "row",
|
||||
alignItems: "center",
|
||||
padding: 20,
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
style={{
|
||||
width: isTablet ? 20 : isMobile ? 30 : 30,
|
||||
height: isTablet ? 20 : isMobile ? 30 : 30
|
||||
}}
|
||||
>
|
||||
<use href="#full-logo" />
|
||||
</svg>
|
||||
<Text
|
||||
variant="heading"
|
||||
sx={{
|
||||
fontSize: 16,
|
||||
display: isTablet ? "none" : "block"
|
||||
}}
|
||||
>
|
||||
<b>Notesnook</b>
|
||||
</Text>
|
||||
</Flex>
|
||||
<NavigationDropdown
|
||||
toggleNavigationContainer={toggleNavigationContainer}
|
||||
/>
|
||||
</Flex>
|
||||
<Flex
|
||||
sx={{
|
||||
flexDirection: isTablet ? "column" : "row",
|
||||
justifyContent: "center"
|
||||
}}
|
||||
>
|
||||
{tabs.map((tab) => (
|
||||
<NavigationItem
|
||||
key={tab.id}
|
||||
id={tab.id}
|
||||
isTablet={isTablet}
|
||||
title={tab.title}
|
||||
icon={tab.icon}
|
||||
selected={currentTab === tab.id}
|
||||
onClick={() => setCurrentTab(tab.id)}
|
||||
showTitle={false}
|
||||
/>
|
||||
))}
|
||||
</Flex>
|
||||
{isTablet && (
|
||||
<Box
|
||||
bg="separator"
|
||||
my={1}
|
||||
sx={{ width: "85%", height: "0.8px", alignSelf: "center" }}
|
||||
/>
|
||||
)}
|
||||
<Flex
|
||||
id="navigation-menu"
|
||||
data-test-id="navigation-menu"
|
||||
@@ -234,7 +337,9 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
flex: 1,
|
||||
overflow: "hidden",
|
||||
flexDirection: "column",
|
||||
justifyContent: "space-between"
|
||||
justifyContent: "space-between",
|
||||
paddingLeft: isTablet ? 0 : 20,
|
||||
paddingRight: isTablet ? 0 : 20
|
||||
}}
|
||||
px={0}
|
||||
onContextMenu={async (e) => {
|
||||
@@ -242,299 +347,419 @@ function NavigationMenu(props: NavigationMenuProps) {
|
||||
Menu.openMenu(await getSidebarItems());
|
||||
}}
|
||||
>
|
||||
<FlexScrollContainer
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
display: "flex"
|
||||
}}
|
||||
trackStyle={() => ({
|
||||
width: 3
|
||||
})}
|
||||
thumbStyle={() => ({ width: 3 })}
|
||||
suppressScrollX={true}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<ReorderableList
|
||||
items={routes.filter((r) => !hiddenRoutes.includes(r.id))}
|
||||
orderKey={`sidebarOrder:routes`}
|
||||
order={() => db.settings.getSideBarOrder("routes")}
|
||||
onOrderChanged={(order) =>
|
||||
db.settings.setSideBarOrder("routes", order)
|
||||
}
|
||||
renderOverlay={({ item }) => (
|
||||
<NavigationItem
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
tag={item.tag}
|
||||
selected={
|
||||
item.path === "/"
|
||||
? location === item.path
|
||||
: location.startsWith(item.path)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
renderItem={({ item }) => (
|
||||
<SortableNavigationItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
tag={item.tag}
|
||||
onDragEnter={() => {
|
||||
if (["/notebooks", "/tags"].includes(item.path))
|
||||
dragTimeout.current = setTimeout(
|
||||
() => _navigate(item.path),
|
||||
1000
|
||||
) as unknown as number;
|
||||
}}
|
||||
onDragLeave={() => clearTimeout(dragTimeout.current)}
|
||||
onDrop={async (e) => {
|
||||
clearTimeout(dragTimeout.current);
|
||||
|
||||
await handleDrop(e.dataTransfer, {
|
||||
type:
|
||||
item.path === "/trash"
|
||||
? "trash"
|
||||
: item.path === "/favorites"
|
||||
? "favorites"
|
||||
: item.path === "/notebooks"
|
||||
? "notebooks"
|
||||
: undefined
|
||||
});
|
||||
}}
|
||||
selected={
|
||||
item.path === "/"
|
||||
? location === item.path
|
||||
: location.startsWith(item.path)
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isMobile && location === item.path)
|
||||
return toggleNavigationContainer();
|
||||
_navigate(item.path);
|
||||
}}
|
||||
menuItems={[
|
||||
{
|
||||
type: "lazy-loader",
|
||||
key: "sidebar-items-loader",
|
||||
items: getSidebarItems
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ReorderableList
|
||||
items={colors.filter((c) => !hiddenColors.includes(c.id))}
|
||||
orderKey={`sidebarOrder:colors`}
|
||||
order={() => db.settings.getSideBarOrder("colors")}
|
||||
onOrderChanged={(order) =>
|
||||
db.settings.setSideBarOrder("colors", order)
|
||||
}
|
||||
renderOverlay={({ item }) => (
|
||||
<NavigationItem
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
title={item.title}
|
||||
icon={Circle}
|
||||
color={item.colorCode}
|
||||
selected={location === `/colors/${item.id}`}
|
||||
/>
|
||||
)}
|
||||
renderItem={({ item: color }) => (
|
||||
<SortableNavigationItem
|
||||
id={color.id}
|
||||
isTablet={isTablet}
|
||||
key={color.id}
|
||||
title={color.title}
|
||||
icon={Circle}
|
||||
selected={location === `/colors/${color.id}`}
|
||||
color={color.colorCode}
|
||||
onClick={() => {
|
||||
_navigate(`/colors/${color.id}`);
|
||||
}}
|
||||
onDrop={(e) => handleDrop(e.dataTransfer, color)}
|
||||
menuItems={[
|
||||
{
|
||||
type: "button",
|
||||
key: "rename-color",
|
||||
title: strings.renameColor(),
|
||||
onClick: () => RenameColorDialog.show(color)
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "remove-color",
|
||||
title: strings.removeColor(),
|
||||
onClick: async () => {
|
||||
await db.colors.remove(color.id);
|
||||
await refreshNavItems();
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
key: "sep"
|
||||
},
|
||||
{
|
||||
type: "lazy-loader",
|
||||
key: "sidebar-items-loader",
|
||||
items: getSidebarItems
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Box
|
||||
bg="separator"
|
||||
my={1}
|
||||
sx={{ width: "85%", height: "0.8px", alignSelf: "center" }}
|
||||
/>
|
||||
<ReorderableList
|
||||
items={shortcuts}
|
||||
orderKey={`sidebarOrder:shortcuts`}
|
||||
order={() => db.settings.getSideBarOrder("shortcuts")}
|
||||
onOrderChanged={(order) =>
|
||||
db.settings.setSideBarOrder("shortcuts", order)
|
||||
}
|
||||
renderOverlay={({ item }) => (
|
||||
<NavigationItem
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
icon={
|
||||
item.type === "notebook"
|
||||
? Notebook2
|
||||
: item.type === "tag"
|
||||
? Tag2
|
||||
: Topic
|
||||
}
|
||||
isShortcut
|
||||
selected={shouldSelectNavItem(location, item)}
|
||||
/>
|
||||
)}
|
||||
renderItem={({ item }) => (
|
||||
<SortableNavigationItem
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
menuItems={[
|
||||
{
|
||||
type: "button",
|
||||
key: "removeshortcut",
|
||||
title: strings.doActions.remove.shortcut(1),
|
||||
onClick: async () => {
|
||||
await db.shortcuts.remove(item.id);
|
||||
refreshNavItems();
|
||||
}
|
||||
}
|
||||
]}
|
||||
icon={
|
||||
item.type === "notebook"
|
||||
? Notebook2
|
||||
: item.type === "tag"
|
||||
? Tag2
|
||||
: Topic
|
||||
}
|
||||
isShortcut
|
||||
selected={shouldSelectNavItem(location, item)}
|
||||
onDrop={(e) => handleDrop(e.dataTransfer, item)}
|
||||
onClick={async () => {
|
||||
if (item.type === "notebook") {
|
||||
const root = (await db.notebooks.breadcrumbs(item.id)).at(
|
||||
0
|
||||
);
|
||||
if (root && root.id !== item.id)
|
||||
_navigate(`/notebooks/${root.id}/${item.id}`);
|
||||
else _navigate(`/notebooks/${item.id}`);
|
||||
} else if (item.type === "tag") {
|
||||
_navigate(`/tags/${item.id}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</FlexScrollContainer>
|
||||
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
{isLoggedIn === false && (
|
||||
<NavigationItem
|
||||
id="login"
|
||||
isTablet={isTablet}
|
||||
title={strings.login()}
|
||||
icon={Login}
|
||||
onClick={() => hardNavigate("/login")}
|
||||
/>
|
||||
)}
|
||||
{isTablet && (
|
||||
<NavigationItem
|
||||
id="change-theme"
|
||||
isTablet={isTablet}
|
||||
title={theme === "dark" ? strings.light() : strings.dark()}
|
||||
icon={theme === "dark" ? LightMode : DarkMode}
|
||||
onClick={() => {
|
||||
setFollowSystemTheme(false);
|
||||
toggleNightMode();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<NavigationItem
|
||||
id={settings.id}
|
||||
isTablet={isTablet}
|
||||
key={settings.path}
|
||||
title={profile?.fullName || settings.title}
|
||||
icon={profile?.fullName ? User : settings.icon}
|
||||
image={profile?.fullName ? profile?.profilePicture : undefined}
|
||||
onClick={() => {
|
||||
if (!isMobile && location === settings.path)
|
||||
return toggleNavigationContainer();
|
||||
hashNavigate("/settings");
|
||||
}}
|
||||
selected={location.startsWith(settings.path)}
|
||||
{currentTab === "notebook" ? (
|
||||
<NotebookTree />
|
||||
) : currentTab === "tag" ? (
|
||||
<Flex
|
||||
sx={{ height: "100vh", gap: 2, my: 1, flexDirection: "column" }}
|
||||
>
|
||||
{isTablet ? null : (
|
||||
<Button
|
||||
variant={"icon"}
|
||||
title={strings.toggleDarkLightMode()}
|
||||
sx={{ borderLeft: "1px solid var(--separator)" }}
|
||||
onClick={() => {
|
||||
setFollowSystemTheme(false);
|
||||
toggleNightMode();
|
||||
}}
|
||||
>
|
||||
{theme === "dark" ? (
|
||||
<LightMode size={16} />
|
||||
) : (
|
||||
<DarkMode size={16} />
|
||||
<Button
|
||||
onClick={CREATE_BUTTON_MAP.tags.onClick}
|
||||
variant="secondary"
|
||||
>
|
||||
{CREATE_BUTTON_MAP.tags.title}
|
||||
</Button>
|
||||
<Tags location="sidebar" />
|
||||
</Flex>
|
||||
) : (
|
||||
<FlexScrollContainer
|
||||
style={{
|
||||
flexDirection: "column",
|
||||
display: "flex"
|
||||
}}
|
||||
trackStyle={() => ({
|
||||
width: 3
|
||||
})}
|
||||
thumbStyle={() => ({ width: 3 })}
|
||||
suppressScrollX={true}
|
||||
>
|
||||
<Flex sx={{ flexDirection: "column" }}>
|
||||
<ReorderableList
|
||||
items={routes.filter((r) => !hiddenRoutes.includes(r.id))}
|
||||
orderKey={`sidebarOrder:routes`}
|
||||
order={() => db.settings.getSideBarOrder("routes")}
|
||||
onOrderChanged={(order) =>
|
||||
db.settings.setSideBarOrder("routes", order)
|
||||
}
|
||||
renderOverlay={({ item }) => (
|
||||
<NavigationItem
|
||||
count={item.count}
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
tag={item.tag}
|
||||
selected={
|
||||
item.path === "/"
|
||||
? location === item.path
|
||||
: location.startsWith(item.path)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</NavigationItem>
|
||||
</Flex>
|
||||
renderItem={({ item }) => (
|
||||
<SortableNavigationItem
|
||||
key={item.id}
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
title={item.title}
|
||||
icon={item.icon}
|
||||
tag={item.tag}
|
||||
count={item.count}
|
||||
onDragEnter={() => {
|
||||
if (["/notebooks", "/tags"].includes(item.path))
|
||||
dragTimeout.current = setTimeout(
|
||||
() => _navigate(item.path),
|
||||
1000
|
||||
) as unknown as number;
|
||||
}}
|
||||
onDragLeave={() => clearTimeout(dragTimeout.current)}
|
||||
onDrop={async (e) => {
|
||||
clearTimeout(dragTimeout.current);
|
||||
|
||||
await handleDrop(e.dataTransfer, {
|
||||
type:
|
||||
item.path === "/trash"
|
||||
? "trash"
|
||||
: item.path === "/favorites"
|
||||
? "favorites"
|
||||
: item.path === "/notebooks"
|
||||
? "notebooks"
|
||||
: undefined
|
||||
});
|
||||
}}
|
||||
selected={
|
||||
item.path === "/"
|
||||
? location === item.path
|
||||
: location.startsWith(item.path)
|
||||
}
|
||||
onClick={() => {
|
||||
if (!isMobile && location === item.path)
|
||||
return toggleNavigationContainer();
|
||||
_navigate(item.path);
|
||||
}}
|
||||
menuItems={[
|
||||
{
|
||||
type: "lazy-loader",
|
||||
key: "sidebar-items-loader",
|
||||
items: getSidebarItems
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
|
||||
<ReorderableList
|
||||
items={colors.filter((c) => !hiddenColors.includes(c.id))}
|
||||
orderKey={`sidebarOrder:colors`}
|
||||
order={() => db.settings.getSideBarOrder("colors")}
|
||||
onOrderChanged={(order) =>
|
||||
db.settings.setSideBarOrder("colors", order)
|
||||
}
|
||||
renderOverlay={({ item }) => (
|
||||
<NavigationItem
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
title={item.title}
|
||||
icon={Circle}
|
||||
color={item.colorCode}
|
||||
count={item.count}
|
||||
selected={location === `/colors/${item.id}`}
|
||||
/>
|
||||
)}
|
||||
renderItem={({ item: color }) => (
|
||||
<SortableNavigationItem
|
||||
id={color.id}
|
||||
isTablet={isTablet}
|
||||
key={color.id}
|
||||
title={color.title}
|
||||
count={color.count}
|
||||
icon={Circle}
|
||||
selected={location === `/colors/${color.id}`}
|
||||
color={color.colorCode}
|
||||
onClick={() => {
|
||||
_navigate(`/colors/${color.id}`);
|
||||
}}
|
||||
onDrop={(e) => handleDrop(e.dataTransfer, color)}
|
||||
menuItems={[
|
||||
{
|
||||
type: "button",
|
||||
key: "rename-color",
|
||||
title: strings.renameColor(),
|
||||
onClick: () => RenameColorDialog.show(color)
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
key: "remove-color",
|
||||
title: strings.removeColor(),
|
||||
onClick: async () => {
|
||||
await db.colors.remove(color.id);
|
||||
await refreshNavItems();
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
key: "sep"
|
||||
},
|
||||
{
|
||||
type: "lazy-loader",
|
||||
key: "sidebar-items-loader",
|
||||
items: getSidebarItems
|
||||
}
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Box
|
||||
bg="separator"
|
||||
my={1}
|
||||
sx={{ width: "85%", height: "0.8px", alignSelf: "center" }}
|
||||
/>
|
||||
<ReorderableList
|
||||
items={shortcuts}
|
||||
orderKey={`sidebarOrder:shortcuts`}
|
||||
order={() => db.settings.getSideBarOrder("shortcuts")}
|
||||
onOrderChanged={(order) =>
|
||||
db.settings.setSideBarOrder("shortcuts", order)
|
||||
}
|
||||
renderOverlay={({ item }) => (
|
||||
<NavigationItem
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
icon={
|
||||
item.type === "notebook"
|
||||
? Notebook2
|
||||
: item.type === "tag"
|
||||
? Tag2
|
||||
: Topic
|
||||
}
|
||||
isShortcut
|
||||
selected={shouldSelectNavItem(location, item)}
|
||||
/>
|
||||
)}
|
||||
renderItem={({ item }) => (
|
||||
<SortableNavigationItem
|
||||
id={item.id}
|
||||
isTablet={isTablet}
|
||||
key={item.id}
|
||||
title={item.title}
|
||||
menuItems={[
|
||||
{
|
||||
type: "button",
|
||||
key: "removeshortcut",
|
||||
title: strings.doActions.remove.shortcut(1),
|
||||
onClick: async () => {
|
||||
await db.shortcuts.remove(item.id);
|
||||
refreshNavItems();
|
||||
}
|
||||
}
|
||||
]}
|
||||
icon={
|
||||
item.type === "notebook"
|
||||
? Notebook2
|
||||
: item.type === "tag"
|
||||
? Tag2
|
||||
: Topic
|
||||
}
|
||||
isShortcut
|
||||
selected={shouldSelectNavItem(location, item)}
|
||||
onDrop={(e) => handleDrop(e.dataTransfer, item)}
|
||||
onClick={async () => {
|
||||
if (item.type === "notebook") {
|
||||
const root = (
|
||||
await db.notebooks.breadcrumbs(item.id)
|
||||
).at(0);
|
||||
if (root && root.id !== item.id)
|
||||
_navigate(`/notebooks/${root.id}/${item.id}`);
|
||||
else _navigate(`/notebooks/${item.id}`);
|
||||
} else if (item.type === "tag") {
|
||||
_navigate(`/tags/${item.id}`);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
</FlexScrollContainer>
|
||||
)}
|
||||
</Flex>
|
||||
</ScopedThemeProvider>
|
||||
);
|
||||
}
|
||||
export default NavigationMenu;
|
||||
|
||||
function findNestedRoute(location: string) {
|
||||
let level = location.split("/").length;
|
||||
let nestedRoute = undefined;
|
||||
const history = Array.from(navigationHistory.keys());
|
||||
for (let i = history.length - 1; i >= 0; --i) {
|
||||
const route = history[i];
|
||||
if (!navigationHistory.get(route)) continue;
|
||||
type NavigationDropdownProps = {
|
||||
toggleNavigationContainer: NavigationMenuProps["toggleNavigationContainer"];
|
||||
};
|
||||
|
||||
const routeLevel = route.split("/").length;
|
||||
if (route.startsWith(location) && routeLevel > level) {
|
||||
level = routeLevel;
|
||||
nestedRoute = route;
|
||||
}
|
||||
}
|
||||
return nestedRoute;
|
||||
function NavigationDropdown({
|
||||
toggleNavigationContainer
|
||||
}: NavigationDropdownProps) {
|
||||
const isMobile = useMobile();
|
||||
const [location] = useLocation();
|
||||
const user = useUserStore((store) => store.user);
|
||||
const profile = useSettingStore((store) => store.profile);
|
||||
const theme = useThemeStore((store) => store.colorScheme);
|
||||
const toggleNightMode = useThemeStore((store) => store.toggleColorScheme);
|
||||
const setFollowSystemTheme = useThemeStore(
|
||||
(store) => store.setFollowSystemTheme
|
||||
);
|
||||
|
||||
const { isPro } = useMemo(() => {
|
||||
const type = user?.subscription?.type;
|
||||
const expiry = user?.subscription?.expiry;
|
||||
if (!expiry) return { isBasic: true, remainingDays: 0 };
|
||||
return {
|
||||
isTrial: type === SUBSCRIPTION_STATUS.TRIAL,
|
||||
isBasic: type === SUBSCRIPTION_STATUS.BASIC,
|
||||
isBeta: type === SUBSCRIPTION_STATUS.BETA,
|
||||
isPro: type === SUBSCRIPTION_STATUS.PREMIUM,
|
||||
isProCancelled: type === SUBSCRIPTION_STATUS.PREMIUM_CANCELED,
|
||||
isProExpired: type === SUBSCRIPTION_STATUS.PREMIUM_EXPIRED
|
||||
};
|
||||
}, [user]);
|
||||
|
||||
const notLoggedIn = Boolean(!user || !user.id);
|
||||
|
||||
return (
|
||||
<Flex sx={{ flexDirection: "row", alignItems: "center", gap: 1 }}>
|
||||
<Flex
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
Menu.openMenu(
|
||||
[
|
||||
{
|
||||
type: "popup",
|
||||
component: () => <UserProfile minimal />,
|
||||
key: "profile"
|
||||
},
|
||||
{
|
||||
type: "separator",
|
||||
key: "sep"
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: strings.login(),
|
||||
icon: Login.path,
|
||||
key: "login",
|
||||
isHidden: !notLoggedIn,
|
||||
onClick: () => hardNavigate("/login")
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: strings.toggleDarkLightMode(),
|
||||
key: "toggle-theme-mode",
|
||||
icon: theme === "dark" ? LightMode.path : DarkMode.path,
|
||||
onClick: () => {
|
||||
setFollowSystemTheme(false);
|
||||
toggleNightMode();
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: strings.upgradeToPro(),
|
||||
icon: Pro.path,
|
||||
key: "upgrade",
|
||||
isHidden: notLoggedIn || isPro
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: settings.title,
|
||||
key: settings.id,
|
||||
icon: settings.icon.path,
|
||||
onClick: () => {
|
||||
if (!isMobile && location === settings.path) {
|
||||
return toggleNavigationContainer();
|
||||
}
|
||||
hashNavigate(settings.path);
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: strings.helpAndSupport(),
|
||||
icon: Documentation.path,
|
||||
key: "help-and-support",
|
||||
onClick: () => {
|
||||
window.open("https://help.notesnook.com/", "_blank");
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "button",
|
||||
title: strings.logout(),
|
||||
icon: Logout.path,
|
||||
key: "logout",
|
||||
isHidden: notLoggedIn,
|
||||
onClick: async () => {
|
||||
const result = await showLogoutConfirmation();
|
||||
if (!result) return;
|
||||
|
||||
if (result.backup) {
|
||||
try {
|
||||
await createBackup({ mode: "partial" });
|
||||
} catch (e) {
|
||||
logger.error(e, "Failed to take backup before logout");
|
||||
if (
|
||||
!(await ConfirmDialog.show({
|
||||
title: strings.failedToTakeBackup(),
|
||||
message: strings.failedToTakeBackupMessage(),
|
||||
negativeButtonText: strings.no(),
|
||||
positiveButtonText: strings.yes()
|
||||
}))
|
||||
)
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
await TaskManager.startTask({
|
||||
type: "modal",
|
||||
title: strings.loggingOut(),
|
||||
subtitle: strings.pleaseWait(),
|
||||
action: () => db.user.logout(true)
|
||||
});
|
||||
showToast("success", strings.loggedOut());
|
||||
}
|
||||
}
|
||||
],
|
||||
{
|
||||
position: {
|
||||
target: e.currentTarget,
|
||||
location: "below",
|
||||
yOffset: 5
|
||||
}
|
||||
}
|
||||
);
|
||||
}}
|
||||
variant="columnCenter"
|
||||
sx={{
|
||||
bg: "shade",
|
||||
mr: 2,
|
||||
size: 35,
|
||||
borderRadius: 80,
|
||||
cursor: "pointer",
|
||||
position: "relative",
|
||||
outline: "1px solid var(--accent)",
|
||||
":hover": {
|
||||
outline: "2px solid var(--accent)"
|
||||
}
|
||||
}}
|
||||
>
|
||||
{!user || !user.id || !profile?.profilePicture ? (
|
||||
<User size={30} />
|
||||
) : (
|
||||
<Image
|
||||
sx={{
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
objectFit: "contain",
|
||||
borderRadius: 80
|
||||
}}
|
||||
src={profile.profilePicture}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
type ReorderableListProps<T> = {
|
||||
|
||||
@@ -32,7 +32,7 @@ type NavigationItemProps = {
|
||||
icon?: Icon;
|
||||
image?: string;
|
||||
color?: SchemeColors;
|
||||
title: string;
|
||||
title?: string;
|
||||
isTablet?: boolean;
|
||||
isLoading?: boolean;
|
||||
isShortcut?: boolean;
|
||||
@@ -41,6 +41,7 @@ type NavigationItemProps = {
|
||||
onClick?: () => void;
|
||||
count?: number;
|
||||
menuItems?: MenuItem[];
|
||||
showTitle?: boolean;
|
||||
};
|
||||
|
||||
function NavigationItem(
|
||||
@@ -64,6 +65,7 @@ function NavigationItem(
|
||||
count,
|
||||
sx,
|
||||
containerRef,
|
||||
showTitle = true,
|
||||
...restProps
|
||||
} = props;
|
||||
const isMobile = useMobile();
|
||||
@@ -130,7 +132,7 @@ function NavigationItem(
|
||||
/>
|
||||
) : Icon ? (
|
||||
<Icon
|
||||
size={isTablet ? 16 : 15}
|
||||
size={isTablet ? 16 : 20}
|
||||
color={color || (selected ? "icon-selected" : "icon")}
|
||||
rotate={isLoading}
|
||||
/>
|
||||
@@ -144,22 +146,22 @@ function NavigationItem(
|
||||
/>
|
||||
)}
|
||||
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
fontWeight: selected ? "bold" : "normal",
|
||||
color: selected ? "paragraph-selected" : "paragraph",
|
||||
fontSize: "subtitle",
|
||||
display: isTablet ? "none" : "block"
|
||||
}}
|
||||
ml={1}
|
||||
data-test-id="title"
|
||||
>
|
||||
{title}
|
||||
{/* {tag && (
|
||||
{showTitle && (
|
||||
<Text
|
||||
variant="body"
|
||||
sx={{
|
||||
whiteSpace: "nowrap",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
fontWeight: selected ? "bold" : "normal",
|
||||
color: selected ? "paragraph-selected" : "paragraph",
|
||||
display: isTablet ? "none" : "block"
|
||||
}}
|
||||
ml={1}
|
||||
data-test-id="title"
|
||||
>
|
||||
{title}
|
||||
{/* {tag && (
|
||||
<Text
|
||||
variant="subBody"
|
||||
as="span"
|
||||
@@ -174,7 +176,8 @@ function NavigationItem(
|
||||
{tag}
|
||||
</Text>
|
||||
)} */}
|
||||
</Text>
|
||||
</Text>
|
||||
)}
|
||||
</Button>
|
||||
{children ? (
|
||||
children
|
||||
|
||||
176
apps/web/src/components/navigation-menu/notebook-tree.tsx
Normal file
176
apps/web/src/components/navigation-menu/notebook-tree.tsx
Normal file
@@ -0,0 +1,176 @@
|
||||
/*
|
||||
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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { Notebook } from "@notesnook/core";
|
||||
import { Button, Flex } from "@theme-ui/components";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { CREATE_BUTTON_MAP } from "../../common";
|
||||
import { db } from "../../common/db";
|
||||
import { store, useStore } from "../../stores/notebook-store";
|
||||
import { useStore as useSelectionStore } from "../../stores/selection-store";
|
||||
import Placeholder from "../placeholders";
|
||||
import SubNotebook from "../sub-notebook";
|
||||
import {
|
||||
TreeNode,
|
||||
VirtualizedTree,
|
||||
VirtualizedTreeHandle
|
||||
} from "../virtualized-tree";
|
||||
import { ListLoader } from "../loaders/list-loader";
|
||||
|
||||
function NotebookTree() {
|
||||
const notebooks = useStore((state) => state.notebooks);
|
||||
const createButton = CREATE_BUTTON_MAP.notebooks;
|
||||
|
||||
useEffect(() => {
|
||||
store.get().refresh();
|
||||
}, []);
|
||||
|
||||
if (!notebooks) return <ListLoader />;
|
||||
if (notebooks.length === 0) {
|
||||
return (
|
||||
<Flex variant="columnCenterFill" data-test-id="list-placeholder">
|
||||
<Placeholder context="notebooks" />
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Flex sx={{ gap: 2, flexDirection: "column", my: 1 }}>
|
||||
<Button variant="secondary" onClick={createButton.onClick}>
|
||||
{createButton.title}
|
||||
</Button>
|
||||
<div style={{ height: "100vh" }}>
|
||||
<Tree />
|
||||
</div>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
export default NotebookTree;
|
||||
|
||||
function Tree() {
|
||||
const treeRef =
|
||||
useRef<VirtualizedTreeHandle<{ notebook: Notebook; totalNotes: number }>>(
|
||||
null
|
||||
);
|
||||
const setSelectedItems = useSelectionStore((store) => store.setSelectedItems);
|
||||
const isSelected = useSelectionStore((store) => store.isSelected);
|
||||
const selectItem = useSelectionStore((store) => store.selectItem);
|
||||
const deselectItem = useSelectionStore((store) => store.deselectItem);
|
||||
const toggleSelection = useSelectionStore(
|
||||
(store) => store.toggleSelectionMode
|
||||
);
|
||||
const notebooks = useStore((store) => store.notebooks);
|
||||
const [notebookIds, setNotebookIds] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
notebooks?.ids().then((ids) => setNotebookIds(ids));
|
||||
treeRef.current?.refresh();
|
||||
}, [notebooks]);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id="notebook-tree"
|
||||
variant="columnFill"
|
||||
sx={{
|
||||
height: "100%"
|
||||
}}
|
||||
>
|
||||
{notebookIds.length > 0 && (
|
||||
<VirtualizedTree
|
||||
rootId={"root"}
|
||||
itemHeight={30}
|
||||
treeRef={treeRef}
|
||||
deselectAll={() => toggleSelection(false)}
|
||||
bulkSelect={setSelectedItems}
|
||||
isSelected={isSelected}
|
||||
onDeselect={deselectItem}
|
||||
onSelect={selectItem}
|
||||
saveKey="notebook-tree"
|
||||
getChildNodes={async (id, depth) => {
|
||||
const nodes: TreeNode<{
|
||||
notebook: Notebook;
|
||||
totalNotes: number;
|
||||
}>[] = [];
|
||||
if (id === "root") {
|
||||
for (const id of notebookIds) {
|
||||
const notebook = (await db.notebooks.notebook(id))!;
|
||||
const totalNotes = await db.relations
|
||||
.from(notebook, "note")
|
||||
.count();
|
||||
const children = await db.relations
|
||||
.from(notebook, "notebook")
|
||||
.count();
|
||||
nodes.push({
|
||||
data: { notebook, totalNotes },
|
||||
depth: depth + 1,
|
||||
hasChildren: children > 0,
|
||||
id,
|
||||
parentId: "root"
|
||||
});
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
const subNotebooks = await db.relations
|
||||
.from({ type: "notebook", id }, "notebook")
|
||||
.resolve();
|
||||
|
||||
for (const notebook of subNotebooks) {
|
||||
const hasChildren =
|
||||
(await db.relations.from(notebook, "notebook").count()) > 0;
|
||||
const totalNotes = await db.relations
|
||||
.from(notebook, "note")
|
||||
.count();
|
||||
nodes.push({
|
||||
parentId: id,
|
||||
id: notebook.id,
|
||||
data: { notebook, totalNotes },
|
||||
depth: depth + 1,
|
||||
hasChildren
|
||||
});
|
||||
}
|
||||
|
||||
return nodes;
|
||||
}}
|
||||
renderItem={({ collapse, expand, expanded, index, item: node }) => (
|
||||
<SubNotebook
|
||||
depth={node.depth}
|
||||
isExpandable={node.hasChildren}
|
||||
item={node.data.notebook}
|
||||
isExpanded={expanded}
|
||||
rootId={node.parentId}
|
||||
totalNotes={node.data.totalNotes}
|
||||
refresh={async () => {
|
||||
const notebook = await db.notebooks.notebook(node.id);
|
||||
const totalNotes = await db.relations
|
||||
.from(node.data.notebook, "note")
|
||||
.count();
|
||||
treeRef.current?.refreshItem(
|
||||
index,
|
||||
notebook ? { notebook, totalNotes } : undefined
|
||||
);
|
||||
}}
|
||||
collapse={collapse}
|
||||
expand={expand}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
@@ -154,7 +154,7 @@ function Header(props: RouteContainerProps) {
|
||||
>
|
||||
<Menu
|
||||
sx={{
|
||||
display: ["block", "none", "none"],
|
||||
display: ["block", "block", "none"],
|
||||
size: 23
|
||||
}}
|
||||
size={24}
|
||||
|
||||
@@ -73,6 +73,10 @@ function SubNotebook(props: SubNotebookProps) {
|
||||
item,
|
||||
totalNotes
|
||||
});
|
||||
if (rootId === "root") {
|
||||
navigate(`/notebooks/${item.id}`);
|
||||
return;
|
||||
}
|
||||
navigate(`/notebooks/${rootId}/${item.id}`);
|
||||
}, [expand, focus, isOpened, item, rootId, totalNotes]);
|
||||
|
||||
|
||||
@@ -85,8 +85,15 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
treeRef,
|
||||
() => ({
|
||||
async refresh() {
|
||||
const children = await getChildNodes(rootId, -1);
|
||||
setNodes(children);
|
||||
// const children = await getChildNodes(rootId, -1);
|
||||
// setNodes(children);
|
||||
const nodes = await fetchChildren(
|
||||
rootId,
|
||||
-1,
|
||||
expandedIds,
|
||||
getChildNodes
|
||||
);
|
||||
setNodes(nodes);
|
||||
},
|
||||
async refreshItem(index, item) {
|
||||
const node = nodes[index];
|
||||
@@ -103,6 +110,11 @@ export function VirtualizedTree<T>(props: TreeViewProps<T>) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: double check
|
||||
if (node.hasChildren) {
|
||||
expandedIds[node.id] = true;
|
||||
}
|
||||
|
||||
const children = await fetchChildren(
|
||||
node.id,
|
||||
node.depth,
|
||||
|
||||
@@ -32,7 +32,11 @@ import { EditProfilePictureDialog } from "../../edit-profile-picture-dialog";
|
||||
import { PromptDialog } from "../../prompt";
|
||||
import { strings } from "@notesnook/intl";
|
||||
|
||||
export function UserProfile() {
|
||||
type Props = {
|
||||
minimal?: boolean;
|
||||
};
|
||||
|
||||
export function UserProfile({ minimal }: Props) {
|
||||
const user = useUserStore((store) => store.user);
|
||||
const profile = useSettingStore((store) => store.profile);
|
||||
|
||||
@@ -67,7 +71,7 @@ export function UserProfile() {
|
||||
alignItems: "center",
|
||||
bg: "var(--background-secondary)",
|
||||
p: 2,
|
||||
mb: 4
|
||||
mb: minimal ? 0 : 4
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
@@ -96,7 +100,7 @@ export function UserProfile() {
|
||||
justifyContent: "space-between",
|
||||
bg: "var(--background-secondary)",
|
||||
p: 2,
|
||||
mb: 4
|
||||
mb: minimal ? 0 : 4
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
@@ -110,7 +114,7 @@ export function UserProfile() {
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
":hover #profile-picture-edit": {
|
||||
visibility: "visible"
|
||||
visibility: minimal ? "hidden" : "visible"
|
||||
}
|
||||
}}
|
||||
>
|
||||
@@ -165,35 +169,43 @@ export function UserProfile() {
|
||||
|
||||
<Text variant={"title"}>
|
||||
{profile?.fullName || strings.yourFullName()}{" "}
|
||||
<Edit
|
||||
sx={{ display: "inline-block", cursor: "pointer" }}
|
||||
size={12}
|
||||
title={strings.editFullName()}
|
||||
onClick={async () => {
|
||||
const fullName = await PromptDialog.show({
|
||||
title: strings.editFullName(),
|
||||
description: strings.setFullNameDesc(),
|
||||
defaultValue: profile?.fullName
|
||||
});
|
||||
|
||||
if (fullName === profile?.fullName) return;
|
||||
|
||||
try {
|
||||
await db.settings.setProfile({
|
||||
fullName: fullName || undefined
|
||||
{minimal ? null : (
|
||||
<Edit
|
||||
sx={{ display: "inline-block", cursor: "pointer" }}
|
||||
size={12}
|
||||
title={strings.editFullName()}
|
||||
onClick={async () => {
|
||||
const fullName = await PromptDialog.show({
|
||||
title: strings.editFullName(),
|
||||
description: strings.setFullNameDesc(),
|
||||
defaultValue: profile?.fullName
|
||||
});
|
||||
await useSettingStore.getState().refresh();
|
||||
showToast("success", strings.fullNameUpdated());
|
||||
} catch (e) {
|
||||
showToast("error", (e as Error).message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
if (fullName === profile?.fullName) return;
|
||||
|
||||
try {
|
||||
await db.settings.setProfile({
|
||||
fullName: fullName || undefined
|
||||
});
|
||||
await useSettingStore.getState().refresh();
|
||||
showToast("success", strings.fullNameUpdated());
|
||||
} catch (e) {
|
||||
showToast("error", (e as Error).message);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Text>
|
||||
<Text variant={"subBody"}>
|
||||
{user.email} •{" "}
|
||||
{strings.memberSince(
|
||||
getFormattedDate(getObjectIdTimestamp(user.id), "date")
|
||||
{user.email}
|
||||
{minimal ? null : (
|
||||
<>
|
||||
{" "}
|
||||
•{" "}
|
||||
{strings.memberSince(
|
||||
getFormattedDate(getObjectIdTimestamp(user.id), "date")
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Text>
|
||||
</Flex>
|
||||
|
||||
@@ -77,10 +77,6 @@ const routes = defineRoutes({
|
||||
// ),
|
||||
buttons: {
|
||||
create: CREATE_BUTTON_MAP.notes,
|
||||
back: {
|
||||
title: strings.goBackToNotebooks(),
|
||||
onClick: () => navigate("/notebooks")
|
||||
},
|
||||
search: {
|
||||
title: strings.searchANote()
|
||||
}
|
||||
@@ -153,6 +149,9 @@ const routes = defineRoutes({
|
||||
title: strings.routes.Tags(),
|
||||
type: "tags",
|
||||
component: Tags,
|
||||
props: {
|
||||
location: "middle-pane"
|
||||
},
|
||||
buttons: {
|
||||
create: CREATE_BUTTON_MAP.tags,
|
||||
search: {
|
||||
@@ -178,10 +177,6 @@ const routes = defineRoutes({
|
||||
component: Notes,
|
||||
buttons: {
|
||||
create: CREATE_BUTTON_MAP.notes,
|
||||
back: {
|
||||
title: strings.goBackToTags(),
|
||||
onClick: () => navigate("/tags")
|
||||
},
|
||||
search: {
|
||||
title: strings.searchANote()
|
||||
}
|
||||
|
||||
@@ -74,7 +74,7 @@ class AppStore extends BaseStore<AppStore> {
|
||||
progress: null,
|
||||
type: undefined
|
||||
};
|
||||
colors: Color[] = [];
|
||||
colors: (Color & { count: number })[] = [];
|
||||
notices: Notice[] = [];
|
||||
shortcuts: (Notebook | Tag)[] = [];
|
||||
lastSynced = 0;
|
||||
@@ -166,9 +166,16 @@ class AppStore extends BaseStore<AppStore> {
|
||||
refreshNavItems = async () => {
|
||||
const shortcuts = await db.shortcuts.resolved();
|
||||
const colors = await db.colors.all.items();
|
||||
|
||||
let newColors: (Color & { count: number })[] = [];
|
||||
for (const color of colors) {
|
||||
const count = await db.colors.count(color.id);
|
||||
newColors.push({ ...color, count: count ?? 0 });
|
||||
}
|
||||
|
||||
this.set((state) => {
|
||||
state.shortcuts = shortcuts;
|
||||
state.colors = colors;
|
||||
state.colors = newColors;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -120,168 +120,11 @@ function Notebook(props: NotebookProps) {
|
||||
}
|
||||
/>
|
||||
</Pane>
|
||||
<Pane id="subnotebooks-pane" initialSize={250} minSize={30}>
|
||||
<SubNotebooks
|
||||
isCollapsed={isCollapsed}
|
||||
rootId={rootId}
|
||||
onClick={() => {
|
||||
if (isCollapsed) pane.current?.expand(1);
|
||||
else pane.current?.collapse(1);
|
||||
}}
|
||||
/>
|
||||
</Pane>
|
||||
</SplitPane>
|
||||
);
|
||||
}
|
||||
export default Notebook;
|
||||
|
||||
type SubNotebooksProps = {
|
||||
rootId: string;
|
||||
isCollapsed: boolean;
|
||||
onClick: () => void;
|
||||
};
|
||||
function SubNotebooks({ rootId, isCollapsed, onClick }: SubNotebooksProps) {
|
||||
// sometimes the onClick event is triggered on dragEnd
|
||||
// which shouldn't happen. To prevent that we make sure
|
||||
// that onMouseDown & onMouseUp events got called.
|
||||
const mouseEventCounter = useRef(0);
|
||||
const treeRef = useRef<VirtualizedTreeHandle<SubNotebookTreeItem>>(null);
|
||||
const setSelectedItems = useSelectionStore((store) => store.setSelectedItems);
|
||||
const isSelected = useSelectionStore((store) => store.isSelected);
|
||||
const selectItem = useSelectionStore((store) => store.selectItem);
|
||||
const deselectItem = useSelectionStore((store) => store.deselectItem);
|
||||
const toggleSelection = useSelectionStore(
|
||||
(store) => store.toggleSelectionMode
|
||||
);
|
||||
const rootNotebook = usePromise(
|
||||
() => db.notebooks.notebook(rootId),
|
||||
[rootId]
|
||||
);
|
||||
const notebooks = useNotebookStore((store) => store.notebooks);
|
||||
|
||||
useEffect(() => {
|
||||
treeRef.current?.refresh();
|
||||
}, [notebooks]);
|
||||
|
||||
if (!rootId) return null;
|
||||
|
||||
return (
|
||||
<Flex
|
||||
id="subnotebooks"
|
||||
variant="columnFill"
|
||||
sx={{
|
||||
height: "100%",
|
||||
borderTop: "1px solid var(--border)"
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
sx={{
|
||||
m: 1,
|
||||
ml: 1,
|
||||
alignItems: "center",
|
||||
justifyContent: "space-between",
|
||||
cursor: "pointer"
|
||||
}}
|
||||
onMouseDown={() => {
|
||||
mouseEventCounter.current = 1;
|
||||
}}
|
||||
onMouseUp={() => {
|
||||
mouseEventCounter.current++;
|
||||
}}
|
||||
onClick={() => {
|
||||
if (mouseEventCounter.current === 2) onClick();
|
||||
mouseEventCounter.current = 0;
|
||||
}}
|
||||
>
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
{isCollapsed ? <ChevronRight size={16} /> : <ChevronDown size={16} />}
|
||||
<Text variant="subBody" sx={{ fontSize: 11 }}>
|
||||
{rootNotebook.status === "fulfilled" && rootNotebook.value
|
||||
? rootNotebook.value.title
|
||||
: ""}
|
||||
</Text>
|
||||
</Flex>
|
||||
<Flex sx={{ alignItems: "center" }}>
|
||||
{/* <Button
|
||||
variant="secondary"
|
||||
data-test-id="subnotebooks-sort-button"
|
||||
sx={{
|
||||
p: "small",
|
||||
bg: "transparent",
|
||||
visibility: isCollapsed ? "collapse" : "visible"
|
||||
}}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
// showSortMenu("notebooks", () => refresh(selectedNotebook.id));
|
||||
}}
|
||||
>
|
||||
<SortAsc size={15} />
|
||||
</Button> */}
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-test-id="subnotebooks-action-button"
|
||||
sx={{
|
||||
p: "1px",
|
||||
bg: "transparent",
|
||||
visibility: isCollapsed ? "collapse" : "visible"
|
||||
}}
|
||||
onClick={async (e) => {
|
||||
e.stopPropagation();
|
||||
const context = useNotesStore.getState().context;
|
||||
await AddNotebookDialog.show({
|
||||
parentId: context?.type === "notebook" ? context.id : rootId
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Plus size={20} />
|
||||
</Button>
|
||||
</Flex>
|
||||
</Flex>
|
||||
|
||||
<VirtualizedTree
|
||||
itemHeight={28}
|
||||
getChildNodes={fetchChildren}
|
||||
rootId={rootId}
|
||||
deselectAll={() => toggleSelection(false)}
|
||||
bulkSelect={setSelectedItems}
|
||||
isSelected={isSelected}
|
||||
onDeselect={deselectItem}
|
||||
onSelect={selectItem}
|
||||
treeRef={treeRef}
|
||||
placeholder={() => (
|
||||
<Text variant="subBody" sx={{ mx: 2 }}>
|
||||
{strings.emptyPlaceholders("notebook")}
|
||||
</Text>
|
||||
)}
|
||||
saveKey={`${rootId}-subnotebooks`}
|
||||
testId="subnotebooks-list"
|
||||
renderItem={({ collapse, expand, expanded, index, item: node }) => (
|
||||
<SubNotebook
|
||||
depth={node.depth}
|
||||
isExpandable={node.hasChildren}
|
||||
item={node.data.notebook}
|
||||
isExpanded={expanded}
|
||||
rootId={rootId}
|
||||
totalNotes={node.data.totalNotes}
|
||||
refresh={async () => {
|
||||
const notebook = await db.notebooks.notebook(node.id);
|
||||
const totalNotes = await db.relations
|
||||
.from(node.data.notebook, "note")
|
||||
.count();
|
||||
treeRef.current?.refreshItem(
|
||||
index,
|
||||
notebook ? { notebook, totalNotes } : undefined
|
||||
);
|
||||
}}
|
||||
collapse={collapse}
|
||||
expand={expand}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
function NotebookHeader({
|
||||
rootId,
|
||||
context
|
||||
@@ -341,7 +184,6 @@ function NotebookHeader({
|
||||
ref={moreCrumbsRef}
|
||||
variant="icon"
|
||||
sx={{ p: 0, flexShrink: 0 }}
|
||||
onClick={() => navigateCrumb("notebooks")}
|
||||
title={strings.notebooks()}
|
||||
>
|
||||
<Notebook2 size={14} />
|
||||
|
||||
@@ -17,42 +17,41 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import ListContainer from "../components/list-container";
|
||||
import { useStore, store } from "../stores/notebook-store";
|
||||
import { hashNavigate } from "../navigation";
|
||||
import { navigate } from "../navigation";
|
||||
import Placeholder from "../components/placeholders";
|
||||
import { useEffect } from "react";
|
||||
import { db } from "../common/db";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
import { Flex } from "@theme-ui/components";
|
||||
|
||||
function Notebooks() {
|
||||
const notebooks = useStore((state) => state.notebooks);
|
||||
const refresh = useStore((state) => state.refresh);
|
||||
const filteredItems = useSearch("notebooks", (query) =>
|
||||
db.lookup.notebooks(query).sorted()
|
||||
);
|
||||
const isCompact = useStore((store) => store.viewMode === "compact");
|
||||
|
||||
useEffect(() => {
|
||||
store.get().refresh();
|
||||
}, []);
|
||||
|
||||
if (notebooks && notebooks.length > 0) {
|
||||
notebooks.item(0).then((item) => {
|
||||
if (item && item?.item) {
|
||||
navigate(`/notebooks/${item.item.id}`);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [notebooks]);
|
||||
|
||||
if (!notebooks) return <ListLoader />;
|
||||
return (
|
||||
<>
|
||||
<ListContainer
|
||||
group="notebooks"
|
||||
refresh={refresh}
|
||||
items={filteredItems || notebooks}
|
||||
placeholder={<Placeholder context="notebooks" />}
|
||||
compact={isCompact}
|
||||
button={{
|
||||
onClick: () => hashNavigate("/notebooks/create")
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
if (notebooks.length === 0) {
|
||||
return (
|
||||
<Flex variant="columnFill" sx={{ overflow: "hidden" }}>
|
||||
<Flex variant="columnCenterFill">
|
||||
<Placeholder context="notebooks" />
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default Notebooks;
|
||||
|
||||
@@ -24,8 +24,14 @@ import Placeholder from "../components/placeholders";
|
||||
import { useSearch } from "../hooks/use-search";
|
||||
import { db } from "../common/db";
|
||||
import { ListLoader } from "../components/loaders/list-loader";
|
||||
import { useEffect } from "react";
|
||||
import { navigate } from "../navigation";
|
||||
|
||||
function Tags() {
|
||||
type Props = {
|
||||
location: "middle-pane" | "sidebar";
|
||||
};
|
||||
|
||||
function Tags({ location }: Props) {
|
||||
useNavigate("tags", () => store.refresh());
|
||||
const tags = useStore((store) => store.tags);
|
||||
const refresh = useStore((store) => store.refresh);
|
||||
@@ -33,10 +39,18 @@ function Tags() {
|
||||
db.lookup.tags(query).sorted()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (location === "sidebar") return;
|
||||
tags?.item(0).then((item) => {
|
||||
if (item && item?.item) {
|
||||
navigate(`/tags/${item.item.id}`);
|
||||
}
|
||||
});
|
||||
}, [tags, location]);
|
||||
|
||||
if (!tags) return <ListLoader />;
|
||||
return (
|
||||
<ListContainer
|
||||
group="tags"
|
||||
refresh={refresh}
|
||||
items={filteredItems || tags}
|
||||
placeholder={<Placeholder context="tags" />}
|
||||
|
||||
@@ -112,6 +112,12 @@ export class Colors implements ICollection {
|
||||
);
|
||||
}
|
||||
|
||||
async count(id: string) {
|
||||
const color = await this.color(id);
|
||||
if (!color) return;
|
||||
return this.db.relations.from(color, "note").count();
|
||||
}
|
||||
|
||||
async remove(...ids: string[]) {
|
||||
await this.db.transaction(async () => {
|
||||
await this.db.relations.unlinkOfType("color", ids);
|
||||
|
||||
Reference in New Issue
Block a user