From 1be0e4790e386c4e7ec1aa2975a564e79054fdd3 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Wed, 21 May 2025 11:31:12 +0500 Subject: [PATCH] web: add 'move to top' for sub-notebooks (#8066) Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../web/__e2e__/models/notebook-item.model.ts | 7 ++++ apps/web/__e2e__/notebooks.test.ts | 27 +++++++++++++ apps/web/src/components/icons/index.tsx | 4 +- apps/web/src/components/notebook/index.tsx | 38 +++++++++++++++---- .../src/dialogs/command-palette/commands.ts | 5 ++- packages/core/__tests__/notebooks.test.js | 11 ++++++ packages/core/src/collections/notebooks.ts | 13 +++++++ 7 files changed, 96 insertions(+), 9 deletions(-) diff --git a/apps/web/__e2e__/models/notebook-item.model.ts b/apps/web/__e2e__/models/notebook-item.model.ts index c437de1e5..06cb82e6a 100644 --- a/apps/web/__e2e__/models/notebook-item.model.ts +++ b/apps/web/__e2e__/models/notebook-item.model.ts @@ -113,4 +113,11 @@ export class NotebookItemModel extends BaseItemModel { await this.contextMenu.open(this.locator); await new ToggleModel(this.page, "menu-button-set-as-default").on(); } + + async isMoveToTopVisible() { + await this.contextMenu.open(this.locator); + return this.contextMenu.menuContainer + .locator(getTestId("menu-button-move-to-top")) + .isVisible(); + } } diff --git a/apps/web/__e2e__/notebooks.test.ts b/apps/web/__e2e__/notebooks.test.ts index 093997686..f264247c9 100644 --- a/apps/web/__e2e__/notebooks.test.ts +++ b/apps/web/__e2e__/notebooks.test.ts @@ -324,3 +324,30 @@ test("when default notebook is set, created note in colors context should go to expect(await openedNotebook?.findNote(coloredNote)).toBeDefined(); }); + +test("move to top option should not be available for root notebook", async ({ + page +}) => { + const app = new AppModel(page); + await app.goto(); + const notebooks = await app.goToNotebooks(); + + const notebook = await notebooks.createNotebook(NOTEBOOK); + + expect(await notebook?.isMoveToTopVisible()).toBe(false); +}); + +test("move to top option should be available for sub-notebook", async ({ + page +}) => { + const app = new AppModel(page); + await app.goto(); + const notebooks = await app.goToNotebooks(); + + const notebook = await notebooks.createNotebook(NOTEBOOK); + const subNotebook = await notebook?.createSubnotebook({ + title: "Subnotebook 1" + }); + + expect(await subNotebook?.isMoveToTopVisible()).toBe(true); +}); diff --git a/apps/web/src/components/icons/index.tsx b/apps/web/src/components/icons/index.tsx index 36487e67a..ec4eae8bc 100644 --- a/apps/web/src/components/icons/index.tsx +++ b/apps/web/src/components/icons/index.tsx @@ -229,7 +229,8 @@ import { mdiArrowCollapseRight, mdiHamburger, mdiNotePlus, - mdiNoteEditOutline + mdiNoteEditOutline, + mdiArrowUp } from "@mdi/js"; import { useTheme } from "@emotion/react"; import { Theme } from "@notesnook/theme"; @@ -582,3 +583,4 @@ export const SessionHistory = createIcon(mdiHistory); export const ColorRemove = createIcon(mdiCloseCircleOutline); export const ExpandSidebar = createIcon(mdiArrowCollapseRight); export const HamburgerMenu = createIcon(mdiMenu); +export const ArrowUp = createIcon(mdiArrowUp); diff --git a/apps/web/src/components/notebook/index.tsx b/apps/web/src/components/notebook/index.tsx index 02501169d..72e483034 100644 --- a/apps/web/src/components/notebook/index.tsx +++ b/apps/web/src/components/notebook/index.tsx @@ -25,12 +25,12 @@ import { ChevronDown, ChevronRight, NotebookEdit, - Pin, Plus, RemoveShortcutLink, Shortcut, Trash, - Notebook as NotebookIcon + Notebook as NotebookIcon, + ArrowUp } from "../icons"; import { MenuItem } from "@notesnook/ui"; import { hashNavigate, navigate } from "../../navigation"; @@ -44,6 +44,7 @@ import { Multiselect } from "../../common/multi-select"; import { strings } from "@notesnook/intl"; import { db } from "../../common/db"; import { createSetDefaultHomepageMenuItem } from "../../common"; +import { useStore as useNotebookStore } from "../../stores/notebook-store"; type NotebookProps = { item: NotebookType; @@ -163,7 +164,7 @@ export function Notebook(props: NotebookProps) { } menuItems={notebookMenuItems} - context={{ refresh }} + context={{ refresh, isRoot: depth === 0 }} sx={{ mb: "small", borderRadius: "default", @@ -176,7 +177,7 @@ export function Notebook(props: NotebookProps) { export const notebookMenuItems: ( notebook: NotebookType, ids?: string[], - context?: { refresh?: () => void } + context?: { refresh?: () => void; isRoot?: boolean } ) => MenuItem[] = (notebook, ids = [], context) => { const defaultNotebook = db.settings.getDefaultNotebook(); return [ @@ -190,8 +191,7 @@ export const notebookMenuItems: ( res ? context?.refresh?.() : null ) }, - { type: "separator", key: "sepep2" }, - + { type: "separator", key: "sep0" }, { type: "button", key: "edit", @@ -231,7 +231,31 @@ export const notebookMenuItems: ( : strings.addShortcut(), onClick: () => appStore.addToShortcuts(notebook) }, - { key: "sep", type: "separator" }, + { key: "sep1", type: "separator" }, + { + type: "button", + key: "move-to-top", + icon: ArrowUp.path, + title: strings.moveToTop(), + isHidden: context?.isRoot, + onClick: async () => { + if (context?.isRoot) return; + + const parentId = await db.notebooks.parentId(notebook.id); + if (!parentId) return; + + await db.relations.unlink( + { + type: "notebook", + id: parentId + }, + notebook + ); + await useNotebookStore.getState().refresh(); + }, + multiSelect: false + }, + { key: "sep2", type: "separator", isHidden: context?.isRoot }, { type: "button", key: "movetotrash", diff --git a/apps/web/src/dialogs/command-palette/commands.ts b/apps/web/src/dialogs/command-palette/commands.ts index dc9cc09df..0d8412ddc 100644 --- a/apps/web/src/dialogs/command-palette/commands.ts +++ b/apps/web/src/dialogs/command-palette/commands.ts @@ -326,7 +326,10 @@ async function getActiveNotebookCommands() { const commands: Command[] = []; - const menuItems = notebookMenuItems(notebook, [notebook.id]); + const parentId = await db.notebooks.parentId(notebook.id); + const menuItems = notebookMenuItems(notebook, [notebook.id], { + isRoot: !parentId + }); for (const menuItem of menuItems) { commands.push(...menuItemToCommands(menuItem, group, "active-notebook")); } diff --git a/packages/core/__tests__/notebooks.test.js b/packages/core/__tests__/notebooks.test.js index 50c72b122..3b9793864 100644 --- a/packages/core/__tests__/notebooks.test.js +++ b/packages/core/__tests__/notebooks.test.js @@ -51,3 +51,14 @@ test("updating notebook with empty title should throw", () => notebookTest().then(async ({ db, id }) => { await expect(db.notebooks.add({ id, title: "" })).rejects.toThrow(); })); + +test("parentId() returns parentId if notebook is a subnotebook", () => + notebookTest().then(async ({ db, id }) => { + const subId = await db.notebooks.add({ title: "Sub", id }); + expect(db.notebooks.parentId(subId)).toBe(id); + })); + +test("parentId() returns undefined if notebook is not a subnotebook", () => + notebookTest().then(async ({ db, id }) => { + expect(db.notebooks.parentId(id)).toBeUndefined(); + })); diff --git a/packages/core/src/collections/notebooks.ts b/packages/core/src/collections/notebooks.ts index 392e1d015..c8de8cd84 100644 --- a/packages/core/src/collections/notebooks.ts +++ b/packages/core/src/collections/notebooks.ts @@ -291,4 +291,17 @@ export class Notebooks implements ICollection { await this.collection.softDelete(ids); }); } + + async parentId(id: string): Promise { + const relation = await this.db.relations + .to( + { + id: id, + type: "notebook" + }, + "notebook" + ) + .get(); + return relation[0]?.fromId; + } }