diff --git a/apps/web/src/app.js b/apps/web/src/app.js index d8b864e28..3d4644089 100644 --- a/apps/web/src/app.js +++ b/apps/web/src/app.js @@ -48,6 +48,7 @@ function App() { )} + - diff --git a/apps/web/src/common/dialogcontroller.js b/apps/web/src/common/dialogcontroller.js index 47a1e8ebb..bd682b666 100644 --- a/apps/web/src/common/dialogcontroller.js +++ b/apps/web/src/common/dialogcontroller.js @@ -51,14 +51,15 @@ export function showEditNotebookDialog(notebookId) { const topics = qclone(nb.topics); delete nb.topics; - // add the edited notebook to db - await notebookStore.add({ ...notebook, ...nb }); + let notebook = await db.notebooks.add(nb); // add or delete topics as required const notebookTopics = db.notebooks.notebook(notebook.id).topics; await notebookTopics.delete(...deletedTopics); await notebookTopics.add(...topics); + notebookStore.refresh(); + showToast("success", "Notebook edited successfully!"); perform(true); }} diff --git a/apps/web/src/components/global-menu-wrapper/index.js b/apps/web/src/components/global-menu-wrapper/index.js index 94a5f6f4f..57b06ec18 100644 --- a/apps/web/src/components/global-menu-wrapper/index.js +++ b/apps/web/src/components/global-menu-wrapper/index.js @@ -5,7 +5,7 @@ import Animated from "../animated"; import { useAnimation } from "framer-motion"; function GlobalMenuWrapper() { - const [items, title, state, closeMenu] = useContextMenu(); + const [items, title, data, state, closeMenu] = useContextMenu(); const animation = useAnimation(); useEffect(() => { if (state === "open") { @@ -35,6 +35,7 @@ function GlobalMenuWrapper() { menuItems={items} state={state} title={title} + data={data} style={{ position: "absolute", display: "none", diff --git a/apps/web/src/components/list-item/index.js b/apps/web/src/components/list-item/index.js index bf0f11885..e371d0fd8 100644 --- a/apps/web/src/components/list-item/index.js +++ b/apps/web/src/components/list-item/index.js @@ -6,10 +6,11 @@ import { useStore as useSelectionStore, } from "../../stores/selection-store"; import { useOpenContextMenu } from "../../utils/useContextMenu"; +import { Profiler } from "react"; function selectMenuItem(isSelected, toggleSelection) { return { - title: isSelected ? "Unselect" : "Select", + title: () => (isSelected ? "Unselect" : "Select"), onClick: () => { const selectionState = selectionStore.get(); if (!selectionState.isSelectionMode) { @@ -64,36 +65,40 @@ function ListItem(props) { ); const menuItems = useMemo(() => { - let items = props.menuItems; + let items = props.menu.items; if (props.selectable) items = [selectMenuItem(isSelected, toggleSelection), ...items]; return items; - }, [props.menuItems, isSelected, toggleSelection, props.selectable]); + }, [props.menu.items, isSelected, toggleSelection, props.selectable]); useEffect(() => { if (!isSelectionMode && isSelected) toggleSelection(); }, [isSelectionMode, toggleSelection, isSelected]); return ( - openContextMenu(e, menuItems, false)} - p={2} - justifyContent="space-between" - sx={{ - height: "inherit", - borderBottom: "1px solid", - borderBottomColor: "border", - cursor: "pointer", - position: "relative", - ":hover": { - backgroundColor: props.bg ? props.bg + "11" : "shade", - }, + { + console.log(id, phase, duration); }} - data-test-id={`${props.item.type}-${props.index}`} > + openContextMenu(e, menuItems, props.menu.extraData, false) + } + p={2} + justifyContent="center" + sx={{ + height: "inherit", + borderBottom: "1px solid", + borderBottomColor: "border", + cursor: "pointer", + position: "relative", + ":hover": { + backgroundColor: props.bg ? props.bg + "11" : "shade", + }, + }} flexDirection="column" onClick={() => { //e.stopPropagation(); @@ -103,43 +108,38 @@ function ListItem(props) { props.onClick(); } }} - sx={{ - width: "90%", - ":hover": { - cursor: "pointer", - }, - }} + data-test-id={`${props.item.type}-${props.index}`} > {props.header} - + {isSelectionMode && ( )} - - {props.title} - - + {props.title} + {props.body} - {props.subBody && props.subBody} - - {props.info} - + {props.footer} + {props.menu && ( + + openContextMenu(event, menuItems, props.menu.extraData, true) + } + /> + )} - {props.menuItems && ( - openContextMenu(event, menuItems, true)} - /> - )} - {props.focused && ( - - EDITING NOW - - )} - + ); } export default ListItem; diff --git a/apps/web/src/components/menu/colors.js b/apps/web/src/components/menu/colors.js index d23198a39..48f362ccd 100644 --- a/apps/web/src/components/menu/colors.js +++ b/apps/web/src/components/menu/colors.js @@ -7,7 +7,7 @@ import { Flex } from "rebass"; import useMobile from "../../utils/use-mobile"; function Colors(props) { - const { id, color } = props.data; + const { id, color } = props.note; const setColor = useStore((store) => store.setColor); const isMobile = useMobile(); return ( diff --git a/apps/web/src/components/menu/index.js b/apps/web/src/components/menu/index.js index 7bac92d29..daf90d800 100644 --- a/apps/web/src/components/menu/index.js +++ b/apps/web/src/components/menu/index.js @@ -1,88 +1,93 @@ import { useAnimation } from "framer-motion"; -import React, { useEffect } from "react"; +import React, { useEffect, useMemo } from "react"; import { Flex, Box, Text } from "rebass"; import { isUserPremium } from "../../common"; import useMobile from "../../utils/use-mobile"; import Animated from "../animated"; function Menu(props) { + const { menuItems, data, closeMenu, id, style, sx } = props; const isMobile = useMobile(); - const Container = isMobile ? MobileMenuContainer : MenuContainer; + const Container = useMemo( + () => (isMobile ? MobileMenuContainer : MenuContainer), + [isMobile] + ); return ( - - {props.menuItems.map( - (item) => - item.visible !== false && ( - { - e.stopPropagation(); - if (props.closeMenu) { - props.closeMenu(); - } + + {menuItems.map( + ({ title, key, onClick, component: Component, color, isPro }) => ( + { + e.stopPropagation(); + if (closeMenu) { + closeMenu(); + } - if (!item.component) { - item.onClick(props.data); - } - }} - flexDirection="row" - alignItems="center" - justifyContent="space-between" - py={"8px"} - px={3} - sx={{ - color: item.color || "text", - cursor: "pointer", - ":hover": { - backgroundColor: "shade", - }, - }} - > - {item.component ? ( - item.component - ) : ( - - {item.title} - - )} - {item.onlyPro && !isUserPremium() && ( - - Pro - - )} - - ) + if (!Component) { + onClick(data); + } + }} + flexDirection="row" + alignItems="center" + justifyContent="space-between" + py={"8px"} + px={3} + sx={{ + color: color || "text", + cursor: "pointer", + ":hover": { + backgroundColor: "shade", + }, + }} + > + {Component ? ( + + ) : ( + + {title(data)} + + )} + {isPro && !isUserPremium() && ( + + Pro + + )} + + ) )} ); } -export default Menu; +export default React.memo(Menu, (prev, next) => { + return prev.state === next.state; +}); -function MenuContainer(props) { +function MenuContainer({ id, style, sx, children }) { return ( @@ -96,14 +101,13 @@ function MenuContainer(props) { > Properties - {props.children} + {children} ); } -function MobileMenuContainer(props) { - const { style, id, state } = props; +function MobileMenuContainer({ style, id, state, children }) { const animation = useAnimation(); useEffect(() => { if (state === "open") { @@ -115,6 +119,7 @@ function MobileMenuContainer(props) { animation.start({ y: 500 }); } }, [state, animation, id]); + return ( Properties - {props.children} + {children} diff --git a/apps/web/src/components/navigation-menu/index.js b/apps/web/src/components/navigation-menu/index.js index 72af62666..144e5e395 100644 --- a/apps/web/src/components/navigation-menu/index.js +++ b/apps/web/src/components/navigation-menu/index.js @@ -167,14 +167,15 @@ function NavigationMenu(props) { menu={{ items: [ { - title: "Remove shortcut", - onClick: async () => { + key: "removeshortcut", + title: () => "Remove shortcut", + onClick: async ({ pin }) => { await db.settings.unpin(pin.id); refreshMenuPins(); }, }, ], - data: pin, + extraData: pin, }} icon={ pin.type === "notebook" diff --git a/apps/web/src/components/navigation-menu/navigation-item.js b/apps/web/src/components/navigation-menu/navigation-item.js index f40f8f963..f83ce3e51 100644 --- a/apps/web/src/components/navigation-menu/navigation-item.js +++ b/apps/web/src/components/navigation-menu/navigation-item.js @@ -19,7 +19,7 @@ function NavigationItem(props) { title={title} onContextMenu={(event) => { if (!props.menu) return; - openContextMenu(event, props.menu.items, props.menu.data, false); + openContextMenu(event, props.menu.items, props.menu.extraData, false); }} onClick={() => { if (isMobile) toggleSideMenu(false); diff --git a/apps/web/src/components/note/index.js b/apps/web/src/components/note/index.js index f6b9c8841..5e2b94119 100644 --- a/apps/web/src/components/note/index.js +++ b/apps/web/src/components/note/index.js @@ -8,7 +8,6 @@ import { store, useStore } from "../../stores/note-store"; import { showPasswordDialog } from "../../common/dialog-controller"; import { COLORS } from "../../common"; import { db } from "../../common/db"; -import { useTheme } from "emotion-theming"; import Colors from "../menu/colors"; import { showExportDialog } from "../../common/dialog-controller"; import { showItemDeletedToast } from "../../common/toasts"; @@ -16,91 +15,17 @@ import { showUnpinnedToast } from "../../common/toasts"; import { showToast } from "../../utils/toast"; import { hashNavigate } from "../../navigation"; -const pin = async (note) => { - await store.pin(note.id); - if (note.pinned) await showUnpinnedToast(note.id, "note"); -}; - -function menuItems(note, context) { - return [ - { title: "colors", component: }, - { - title: "Add to notebook", - onClick: async () => { - await showMoveNoteDialog([note.id]); - }, - }, - { - title: note.pinned ? "Unpin" : "Pin", - onClick: async () => { - await pin(note); - }, - }, - { - title: note.favorite ? "Unfavorite" : "Favorite", - onClick: () => store.favorite(note), - }, - { - title: "Export", - onClick: async () => { - if (await showExportDialog([note.id])) - showToast("success", `Note exported successfully!`); - }, - onlyPro: true, - }, - { - title: note.locked ? "Unlock" : "Lock", - onClick: async () => { - const { unlock, lock } = store.get(); - if (!note.locked) { - if (await lock(note.id)) - showToast("success", "Note locked successfully!"); - } else { - if (await unlock(note.id)) - showToast("success", "Note unlocked successfully!"); - } - }, - onlyPro: true, - }, - { - visible: context?.type === "topic", - title: "Remove from topic", - onClick: async () => { - console.log("Remove from topic:", Object.isExtensible(note)); - await db.notebooks - .notebook(context.value.id) - .topics.topic(context.value.topic) - .delete(note.id); - store.setContext(context); - await showToast("success", "Note removed from topic!"); - }, - }, - { - title: "Move to Trash", - color: "red", - onClick: async () => { - if (note.locked) { - const res = await showPasswordDialog("unlock_note", (password) => { - return db.vault - .unlock(password) - .then(() => true) - .catch(() => false); - }); - if (!res) return; - } - await store.delete(note.id).then(() => showItemDeletedToast(note)); - }, - }, - ]; -} - function Note(props) { - const { item, index, pinnable } = props; + const { item, index, context } = props; const note = item; const selectedNote = useStore((store) => store.selectedNote); const isOpened = selectedNote === note.id; - const theme = useTheme(); - const color = useMemo(() => COLORS[note.color], [note.color]); + const [shade, primary] = useMemo(() => { + if (!note.color) return ["shade", "primary"]; + const noteColor = COLORS[note.color]; + return [noteColor + "11", noteColor]; + }, [note.color]); + const notebook = useMemo( () => !!note.notebooks?.length && @@ -111,33 +36,15 @@ function Note(props) { return ( - - - {notebook.title} - - - ) - } - bg={color} + menu={{ + items: context?.type === "topic" ? topicNoteMenuItems : menuItems, + extraData: { note, context }, + }} onClick={() => { if (note.conflicted) { hashNavigate(`/notes/${note.id}/conflict`, true); @@ -147,8 +54,23 @@ function Note(props) { hashNavigate(`/notes/${note.id}/edit`, true); } }} - info={ - <> + header={ + notebook && ( + + + + {notebook.title} + + + ) + } + footer={ + {note.conflicted && ( )} - - {note.pinned && !props.context && ( - - )} - + )} + + {note.locked && ( + - {note.locked && ( - - )} - {note.favorite && ( - - )} - - + )} + {note.favorite && ( + + )} + {isOpened && ( + + EDITING NOW + + )} + } - pinned={pinnable && note.pinned} - menuItems={menuItems(note, props.context)} /> ); } @@ -210,3 +141,95 @@ export default React.memo(Note, function (prevProps, nextProps) { prevItem.notebooks?.length === nextItem.notebooks?.length ); }); + +const pin = async (note) => { + await store.pin(note.id); + if (note.pinned) await showUnpinnedToast(note.id, "note"); +}; + +const menuItems = [ + { + key: "colors", + title: () => "Colors", + component: ({ data }) => , + }, + { + key: "addtonotebook", + title: () => "Add to notebook", + onClick: async ({ note }) => { + await showMoveNoteDialog([note.id]); + }, + }, + { + key: "pin", + title: ({ note }) => (note.pinned ? "Unpin" : "Pin"), + onClick: async ({ note }) => { + await pin(note); + }, + }, + { + key: "favorite", + title: ({ note }) => (note.favorite ? "Unfavorite" : "Favorite"), + onClick: ({ note }) => store.favorite(note), + }, + { + key: "export", + title: () => "Export", + onClick: async ({ note }) => { + if (await showExportDialog([note.id])) + showToast("success", `Note exported successfully!`); + }, + isPro: true, + }, + { + key: "unlocknote", + title: ({ note }) => (note.locked ? "Unlock" : "Lock"), + onClick: async ({ note }) => { + const { unlock, lock } = store.get(); + if (!note.locked) { + if (await lock(note.id)) + showToast("success", "Note locked successfully!"); + } else { + if (await unlock(note.id)) + showToast("success", "Note unlocked successfully!"); + } + }, + isPro: true, + }, + { + key: "movetotrash", + title: () => "Move to Trash", + color: "red", + onClick: async ({ note }) => { + if (note.locked) { + const res = await showPasswordDialog("unlock_note", (password) => { + return db.vault + .unlock(password) + .then(() => true) + .catch(() => false); + }); + if (!res) return; + } + await store.delete(note.id).then(() => showItemDeletedToast(note)); + }, + }, +]; + +const topicNoteMenuItems = [ + ...menuItems, + [ + { + key: "removefromtopic", + title: "Remove from topic", + onClick: async ({ note, context }) => { + console.log("Remove from topic:", Object.isExtensible(note)); + await db.notebooks + .notebook(context.value.id) + .topics.topic(context.value.topic) + .delete(note.id); + store.setContext(context); + await showToast("success", "Note removed from topic!"); + }, + }, + ], +]; diff --git a/apps/web/src/components/notebook/index.js b/apps/web/src/components/notebook/index.js index a06911525..de39ffc54 100644 --- a/apps/web/src/components/notebook/index.js +++ b/apps/web/src/components/notebook/index.js @@ -8,39 +8,6 @@ import { db } from "../../common/db"; import * as Icon from "../icons"; import { hashNavigate } from "../../navigation"; -const pin = async (notebook, index) => { - await store.pin(notebook, index); - if (notebook.pinned) showUnpinnedToast(notebook.id, "notebook"); -}; - -function menuItems(notebook, index) { - return [ - { - title: notebook.pinned ? "Unpin" : "Pin", - onClick: () => pin(notebook, index), - }, - { - title: db.settings.isPinned(notebook.id) - ? "Remove shortcut" - : "Create shortcut", - onClick: () => appStore.pinItemToMenu(notebook), - }, - { - title: "Edit", - onClick: () => hashNavigate(`/notebooks/${notebook.id}/edit`), - }, - { - title: "Move to trash", - color: "red", - onClick: async () => { - await store - .delete(notebook.id, index) - .then(() => showItemDeletedToast(notebook)); - }, - }, - ]; -} - class Notebook extends React.Component { shouldComponentUpdate(nextProps) { const prevItem = this.props.item; @@ -61,49 +28,80 @@ class Notebook extends React.Component { onClick={onClick} title={notebook.title} body={notebook.description} - subBody={ - - {notebook.topics.slice(1, 4).map((topic) => ( - { - onTopicClick(notebook, topic.id); - e.stopPropagation(); - }} - bg="primary" - px={1} - sx={{ - marginRight: 1, - borderRadius: "default", - color: "static", - paddingTop: "2px", - paddingBottom: "2px", - }} - > - - {topic.title} - - - ))} - - } - info={ - - {notebook.pinned && ( - - )} - {new Date(notebook.dateCreated).toDateString().substring(4)} - - • - - {notebook.totalNotes} Notes - - } - pinned={notebook.pinned} index={index} - menuItems={menuItems(notebook, index)} + menu={{ items: menuItems, extraData: { notebook } }} + footer={ + <> + + {notebook.topics.slice(1, 4).map((topic) => ( + { + onTopicClick(notebook, topic.id); + e.stopPropagation(); + }} + bg="primary" + px={1} + sx={{ + marginRight: 1, + borderRadius: "default", + color: "static", + paddingTop: "2px", + paddingBottom: "2px", + }} + > + + {topic.title} + + + ))} + + + {notebook.pinned && ( + + )} + {new Date(notebook.dateCreated).toDateString().substring(4)} + + • + + {notebook.totalNotes} Notes + + + } /> ); } } export default Notebook; + +const pin = async (notebook) => { + await store.pin(notebook); + if (notebook.pinned) showUnpinnedToast(notebook.id, "notebook"); +}; + +const menuItems = [ + { + title: () => "Edit", + onClick: ({ notebook }) => hashNavigate(`/notebooks/${notebook.id}/edit`), + }, + { + key: "pinnotebook", + title: ({ notebook }) => (notebook.pinned ? "Unpin" : "Pin"), + onClick: ({ notebook }) => pin(notebook), + }, + { + key: "shortcut", + title: ({ notebook }) => + db.settings.isPinned(notebook.id) ? "Remove shortcut" : "Create shortcut", + onClick: ({ notebook }) => appStore.pinItemToMenu(notebook), + }, + { + title: () => "Move to trash", + color: "red", + onClick: async ({ notebook }) => { + await store + .delete(notebook.id) + .then(() => showItemDeletedToast(notebook)); + }, + }, +]; diff --git a/apps/web/src/components/route-container/index.js b/apps/web/src/components/route-container/index.js index 45c54785d..79d147863 100644 --- a/apps/web/src/components/route-container/index.js +++ b/apps/web/src/components/route-container/index.js @@ -8,11 +8,11 @@ import useMobile from "../../utils/use-mobile"; import { navigate } from "../../navigation"; function RouteContainer(props) { - const { id, type, title, subtitle, buttons } = props; + const { id, type, title, subtitle, buttons, component } = props; return ( <>
- + {component || } ); } diff --git a/apps/web/src/components/topic/index.js b/apps/web/src/components/topic/index.js index b055b67bd..47bc3ad3b 100644 --- a/apps/web/src/components/topic/index.js +++ b/apps/web/src/components/topic/index.js @@ -4,30 +4,7 @@ import { db } from "../../common/db"; import { store } from "../../stores/notebook-store"; import { store as appStore } from "../../stores/app-store"; import { hashNavigate } from "../../navigation"; - -const menuItems = (item) => [ - { - title: db.settings.isPinned(item.id) - ? "Remove shortcut" - : "Create shortcut", - onClick: () => appStore.pinItemToMenu(item), - }, - { - title: "Edit", - onClick: () => - hashNavigate(`/notebooks/${item.notebookId}/topics/${item.id}/edit`), - visible: item.title !== "General", - }, - { - title: "Delete", - visible: item.title !== "General", - color: "red", - onClick: async () => { - await db.notebooks.notebook(item.notebookId).topics.delete(item.id); - store.setSelectedNotebook(item.notebookId); - }, - }, -]; +import { Text } from "rebass"; function Topic({ item, index, onClick }) { const topic = item; @@ -37,15 +14,47 @@ function Topic({ item, index, onClick }) { item={topic} onClick={onClick} title={topic.title} - info={`${topic.totalNotes} Notes`} + footer={{topic.totalNotes} Notes} index={index} - menuItems={menuItems(topic)} + menu={{ + items: topic.title === "General" ? generalTopicMenuItems : menuItems, + extraData: { topic }, + }} /> ); } + export default React.memo(Topic, (prev, next) => { return ( prev?.item?.title === next?.item?.title && prev?.item?.totalNotes === next?.item?.totalNotes ); }); + +const generalTopicMenuItems = [ + { + key: "shortcut", + title: ({ topic }) => + db.settings.isPinned(topic.id) ? "Remove shortcut" : "Create shortcut", + onClick: ({ topic }) => appStore.pinItemToMenu(topic), + }, +]; + +const menuItems = [ + { + key: "edit", + title: () => "Edit", + onClick: ({ topic }) => + hashNavigate(`/notebooks/${topic.notebookId}/topics/${topic.id}/edit`), + }, + ...generalTopicMenuItems, + { + key: "delete", + title: () => "Delete", + color: "red", + onClick: async ({ topic }) => { + await db.notebooks.notebook(topic.notebookId).topics.delete(topic.id); + store.setSelectedNotebook(topic.notebookId); + }, + }, +]; diff --git a/apps/web/src/stores/notebook-store.js b/apps/web/src/stores/notebook-store.js index 0ea9a2e3c..ce68edb6f 100644 --- a/apps/web/src/stores/notebook-store.js +++ b/apps/web/src/stores/notebook-store.js @@ -13,18 +13,9 @@ class NotebookStore extends BaseStore { this.set((state) => (state.notebooks = db.notebooks.all)); }; - add = async (nb) => { - let notebook = await db.notebooks.add(nb); - if (notebook) { - this.refresh(); - } - }; - - delete = async (id, index) => { + delete = async (id) => { await db.notebooks.delete(id); - this.set((state) => { - state.notebooks.splice(index, 1); - }); + this.refresh(); appStore.refreshMenuPins(); }; diff --git a/apps/web/src/utils/useContextMenu.js b/apps/web/src/utils/useContextMenu.js index b05c79103..b053e6975 100644 --- a/apps/web/src/utils/useContextMenu.js +++ b/apps/web/src/utils/useContextMenu.js @@ -94,15 +94,17 @@ function useContextMenu() { const [items, setItems] = useState([]); const [title, setTitle] = useState(); const [state, setState] = useState(); + const [data, setData] = useState(); useEffect(() => { const onGlobalContextMenu = (e) => { - const { items, title, internalEvent, state } = e.detail; + const { items, title, internalEvent, data, state } = e.detail; setState(state); if (state === "close") { return; } setItems(items); setTitle(title); + setData(data); openMenu(internalEvent); }; window.addEventListener("globalcontextmenu", onGlobalContextMenu); @@ -110,7 +112,7 @@ function useContextMenu() { window.removeEventListener("globalcontextmenu", onGlobalContextMenu); }; }, []); - return [items, title, state, closeMenu]; + return [items, title, data, state, closeMenu]; } export function useOpenContextMenu() {