From b0c18a8ece41c259afdfc7b675d8cf6bb58f1367 Mon Sep 17 00:00:00 2001 From: 01zulfi <85733202+01zulfi@users.noreply.github.com> Date: Tue, 30 Dec 2025 13:27:10 +0500 Subject: [PATCH] web: allow saving note from status icon click & tab menu (#8316) * web: allow saving note from status icon click & tab menu Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> * web: directly save content instead of using events --------- Co-authored-by: Abdullah Atta --- apps/web/__e2e__/editor.test.ts | 38 +++++++++++++++++++ apps/web/__e2e__/models/tab-item.model.ts | 5 +++ apps/web/src/components/editor/action-bar.tsx | 22 ++++++++++- apps/web/src/components/editor/footer.tsx | 25 +++++++++++- apps/web/src/components/editor/index.tsx | 2 +- apps/web/src/components/editor/tiptap.tsx | 1 + packages/intl/locale/en.po | 4 ++ packages/intl/locale/pseudo-LOCALE.po | 4 ++ packages/intl/src/strings.ts | 3 +- 9 files changed, 99 insertions(+), 5 deletions(-) diff --git a/apps/web/__e2e__/editor.test.ts b/apps/web/__e2e__/editor.test.ts index 844e569a1..1a14e5e97 100644 --- a/apps/web/__e2e__/editor.test.ts +++ b/apps/web/__e2e__/editor.test.ts @@ -415,6 +415,44 @@ test("when autosave is disabled, closing the note should save it", async ({ expect(await notes.editor.getContent("text")).toBe(content.trim()); }); +test("when autosave is disabled, clicking the not saved icon should save the note", async ({ + page +}) => { + const app = new AppModel(page); + await app.goto(); + const notes = await app.goToNotes(); + const content = "a ".repeat(100); + await notes.createNote({ + title: NOTE.title, + content + }); + + await notes.editor.notSavedIcon.waitFor({ state: "visible" }); + await notes.editor.notSavedIcon.click(); + + await expect(notes.editor.savedIcon).toBeVisible(); + expect(await notes.editor.getContent("text")).toBe(content.trim()); +}); + +test("when autosave is disabled, clicking save from tab menu should save the note", async ({ + page +}) => { + const app = new AppModel(page); + await app.goto(); + const notes = await app.goToNotes(); + const content = "a ".repeat(100); + await notes.createNote({ + title: NOTE.title, + content + }); + + const tab = (await notes.editor.getTabs())[0]; + await tab.contextMenu.save(); + + await expect(notes.editor.savedIcon).toBeVisible(); + expect(await notes.editor.getContent("text")).toBe(content.trim()); +}); + test("control + alt + right arrow should go to next note", async ({ page }) => { const app = new AppModel(page); await app.goto(); diff --git a/apps/web/__e2e__/models/tab-item.model.ts b/apps/web/__e2e__/models/tab-item.model.ts index 4c9afd74b..3e0849bb5 100644 --- a/apps/web/__e2e__/models/tab-item.model.ts +++ b/apps/web/__e2e__/models/tab-item.model.ts @@ -74,4 +74,9 @@ class TabContextMenuModel { await this.open(); return this.menu.getItem("reveal-in-list"); } + + async save() { + await this.open(); + return this.menu.clickOnItem("save"); + } } diff --git a/apps/web/src/components/editor/action-bar.tsx b/apps/web/src/components/editor/action-bar.tsx index ed6f98965..752fbb3e3 100644 --- a/apps/web/src/components/editor/action-bar.tsx +++ b/apps/web/src/components/editor/action-bar.tsx @@ -83,6 +83,7 @@ import useTablet from "../../hooks/use-tablet"; import { isMac } from "../../utils/platform"; import { CREATE_BUTTON_MAP } from "../../common"; import { getDragData } from "../../utils/data-transfer"; +import { saveContent } from "./index"; type ToolButton = { title: string; @@ -406,6 +407,13 @@ const TabStrip = React.memo(function TabStrip() { isLocked={isLockedSession(session)} isRevealInListDisabled={isFocusMode} type={session.type} + onSave={() => { + const { activeEditorId, getEditor } = + useEditorManager.getState(); + const editor = getEditor(activeEditorId || "")?.editor; + if (!editor) return; + saveContent(session.id, false, editor.getContent()); + }} onFocus={() => { if (tab.id !== currentTab) { useEditorStore.getState().activateSession(tab.sessionId); @@ -486,6 +494,7 @@ type TabProps = { onCloseToTheLeft: () => void; onCloseAll: () => void; onPin: () => void; + onSave: () => void; onRevealInList?: () => void; }; function Tab(props: TabProps) { @@ -505,7 +514,8 @@ function Tab(props: TabProps) { onCloseToTheRight, onCloseToTheLeft, onRevealInList, - onPin + onPin, + onSave } = props; const Icon = isLocked ? type === "locked" @@ -580,6 +590,14 @@ function Tab(props: TabProps) { onContextMenu={(e) => { e.preventDefault(); Menu.openMenu([ + { + type: "button", + title: strings.save(), + key: "save", + onClick: onSave, + isHidden: !isUnsaved + }, + { type: "separator", key: "sep0", isHidden: !isUnsaved }, { type: "button", title: strings.close(), @@ -610,7 +628,7 @@ function Tab(props: TabProps) { key: "close-all", onClick: onCloseAll }, - { type: "separator", key: "sep" }, + { type: "separator", key: "sep1" }, { type: "button", title: strings.revealInList(), diff --git a/apps/web/src/components/editor/footer.tsx b/apps/web/src/components/editor/footer.tsx index 1e23466f3..26393f1f7 100644 --- a/apps/web/src/components/editor/footer.tsx +++ b/apps/web/src/components/editor/footer.tsx @@ -34,7 +34,11 @@ import { NormalMode, Cross } from "../icons"; -import { useEditorConfig, useNoteStatistics } from "./manager"; +import { + useEditorConfig, + useEditorManager, + useNoteStatistics +} from "./manager"; import { getFormattedDate } from "@notesnook/common"; import { MAX_AUTO_SAVEABLE_WORDS, NoteStatistics } from "./types"; import { strings } from "@notesnook/intl"; @@ -43,6 +47,7 @@ import { useWindowControls } from "../../hooks/use-window-controls"; import { exitFullscreen } from "../../utils/fullscreen"; import { useRef, useState } from "react"; import { PopupPresenter } from "@notesnook/ui"; +import { saveContent } from "./index"; const SAVE_STATE_ICON_MAP = { "-1": NotSaved, @@ -276,6 +281,24 @@ function EditorFooter() { ? "icon-error" : "paragraph" } + title={ + saveState === SaveState.NotSaved ? strings.clickToSave() : undefined + } + sx={{ + height: "100%", + cursor: saveState === SaveState.NotSaved ? "pointer" : "default", + ":hover": { + bg: saveState === SaveState.NotSaved ? "hover" : "initial" + } + }} + onClick={() => { + if (saveState === SaveState.NotSaved) { + const { activeEditorId, getEditor } = useEditorManager.getState(); + const editor = getEditor(activeEditorId || "")?.editor; + if (!editor) return; + saveContent(session.id, false, editor.getContent()); + } + }} /> )} diff --git a/apps/web/src/components/editor/index.tsx b/apps/web/src/components/editor/index.tsx index cb960e5b3..a18dc55f9 100644 --- a/apps/web/src/components/editor/index.tsx +++ b/apps/web/src/components/editor/index.tsx @@ -84,7 +84,7 @@ const PDFPreview = React.lazy(() => import("../pdf-preview")); const autoSaveToast = { show: true, hide: () => {} }; -async function saveContent( +export async function saveContent( noteId: string, ignoreEdit: boolean, content: string diff --git a/apps/web/src/components/editor/tiptap.tsx b/apps/web/src/components/editor/tiptap.tsx index 5e3e2712f..a1ee28523 100644 --- a/apps/web/src/components/editor/tiptap.tsx +++ b/apps/web/src/components/editor/tiptap.tsx @@ -68,6 +68,7 @@ import { showFeatureNotAllowedToast } from "../../common/toasts"; import { UpgradeDialog } from "../../dialogs/buy-dialog/upgrade-dialog"; import { ConfirmDialog } from "../../dialogs/confirm"; import { strings } from "@notesnook/intl"; +import { AppEventManager, AppEvents } from "../../common/app-events"; export type OnChangeHandler = ( content: () => string, diff --git a/packages/intl/locale/en.po b/packages/intl/locale/en.po index b40a97b48..1e7d503ac 100644 --- a/packages/intl/locale/en.po +++ b/packages/intl/locale/en.po @@ -1604,6 +1604,10 @@ msgstr "Click to remove" msgid "Click to reset {title}" msgstr "Click to reset {title}" +#: src/strings.ts:2614 +msgid "Click to save" +msgstr "Click to save" + #: src/strings.ts:2610 msgid "Click to update" msgstr "Click to update" diff --git a/packages/intl/locale/pseudo-LOCALE.po b/packages/intl/locale/pseudo-LOCALE.po index 669131bca..e99503895 100644 --- a/packages/intl/locale/pseudo-LOCALE.po +++ b/packages/intl/locale/pseudo-LOCALE.po @@ -1593,6 +1593,10 @@ msgstr "" msgid "Click to reset {title}" msgstr "" +#: src/strings.ts:2614 +msgid "Click to save" +msgstr "" + #: src/strings.ts:2610 msgid "Click to update" msgstr "" diff --git a/packages/intl/src/strings.ts b/packages/intl/src/strings.ts index 525f176e0..87fe73948 100644 --- a/packages/intl/src/strings.ts +++ b/packages/intl/src/strings.ts @@ -2610,5 +2610,6 @@ Use this if changes from other devices are not appearing on this device. This wi clickToUpdate: () => t`Click to update`, noPassword: () => t`No password`, publishToTheWeb: () => t`Publish to the web`, - addToHome: () => t`Add to home` + addToHome: () => t`Add to home`, + clickToSave: () => t`Click to save` };