Merge pull request #9686 from streetwriters/web/clear-trash

web: add clear trash option in trash nav item context menu
This commit is contained in:
Abdullah Atta
2026-04-15 12:48:50 +05:00
committed by GitHub
16 changed files with 909 additions and 700 deletions

View File

@@ -28,7 +28,7 @@ import {
ToastManager
} from "../../../services/event-manager";
import SettingsService from "../../../services/settings";
import { eCloseSheet } from "../../../utils/events";
import { eAfterSync, eCloseSheet } from "../../../utils/events";
import { SideMenuItem } from "../../../utils/menu-items";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
@@ -37,12 +37,17 @@ import AppIcon from "../../ui/AppIcon";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import PaywallSheet from "../paywall";
import { presentDialog } from "../../dialog/functions";
import { db } from "../../../common/database";
import { useTrashStore } from "../../../stores/use-trash-store";
export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
const { colors } = useThemeColors();
const featuresAvailable = useAreFeaturesAvailable([
"customHomepage",
"customizableSidebar"
]);
const trash = useTrashStore((state) => state.items);
return !featuresAvailable ? null : (
<View
style={{
@@ -100,7 +105,37 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
},
icon: "sort-ascending",
locked: !featuresAvailable?.customizableSidebar.isAllowed
}
},
...(item.id === "Trash"
? [
{
title: strings.clearTrash(),
onPress: async () => {
if (!trash || trash?.length === 0) return;
eSendEvent(eCloseSheet);
setTimeout(() => {
presentDialog({
title: strings.clearTrashConfirm(),
paragraph: strings.clearTrashDesc(),
positiveText: strings.clear(),
positivePress: async () => {
await db.trash.clear();
useTrashStore.getState().clear();
eSendEvent(eAfterSync);
ToastManager.show({
message: strings.trashCleared(),
type: "success"
});
return true;
}
});
}, 500);
},
icon: "delete-sweep-outline",
disabled: !trash || trash?.length === 0
}
]
: [])
].map((item) => (
<Pressable
key={item.title}
@@ -112,7 +147,7 @@ export const MenuItemProperties = ({ item }: { item: SideMenuItem }) => {
gap: DefaultAppStyles.GAP_SMALL,
borderRadius: 0,
paddingHorizontal: DefaultAppStyles.GAP,
opacity: item.locked ? 0.6 : 1
opacity: item.disabled || item.locked ? 0.6 : 1
}}
onPress={() => {
item.onPress();

View File

@@ -382,6 +382,16 @@ export const useActions = ({
}
const deleteItem = async () => {
if (isPublished) {
ToastManager.show({
heading: strings.notePublished(),
message: strings.unpublishToDelete(),
type: "error",
context: "local"
});
return;
}
close();
await sleep(300);
@@ -1237,7 +1247,8 @@ export const useActions = ({
: strings.moveToTrash(),
icon: "delete-outline",
type: "error",
onPress: deleteItem
onPress: deleteItem,
locked: isPublished
});
}

View File

@@ -36,7 +36,7 @@ import { strings } from "@notesnook/intl";
const onPressFloatingButton = () => {
presentDialog({
title: strings.clearTrash(),
paragraph: strings.clearTrashConfirm(),
paragraph: strings.clearTrashDesc(),
positiveText: strings.clear(),
negativeText: strings.cancel(),
positivePress: async () => {
@@ -58,8 +58,8 @@ const PLACEHOLDER_DATA = (trashCleanupInterval = 7) => ({
trashCleanupInterval === -1
? strings.noTrashCleanupInterval()
: trashCleanupInterval === 1
? strings.trashCleanupIntervalTextDaily()
: strings.trashCleanupIntervalTextDays(trashCleanupInterval),
? strings.trashCleanupIntervalTextDaily()
: strings.trashCleanupIntervalTextDays(trashCleanupInterval),
loading: strings.loadingTrash()
});
@@ -107,6 +107,7 @@ export const Trash = ({ navigation, route }: NavigationProps<"Trash">) => {
{trash && trash?.placeholders?.length !== 0 ? (
<FloatingButton
onPress={onPressFloatingButton}
testID="trash-clear"
alwaysVisible={true}
/>
) : null}

View File

@@ -34,6 +34,7 @@ import { useRelationStore } from "../stores/use-relation-store";
import { useTagStore } from "../stores/use-tag-store";
import { eUpdateNoteInEditor } from "./events";
import { unlockVault } from "./unlock-vault";
import { useTrashStore } from "../stores/use-trash-store";
export const valueLimiter = (value: number, min: number, max: number) => {
return value < min ? min : value > max ? max : value;
@@ -210,6 +211,7 @@ export const deleteItems = async (
if (type === "notebook") {
useNotebookStore.getState().refresh();
}
useTrashStore.getState().refresh();
useMenuStore.getState().setMenuPins();
};

View File

@@ -0,0 +1,53 @@
import { notesnook } from "../test.ids";
import { TestBuilder } from "./utils";
describe("TRASH TESTS", () => {
it("Move a note to trash", async () => {
await TestBuilder.create()
.prepare()
.createNote("Test", "Test note item")
.tapById(notesnook.listitem.menu)
.tapById("icon-trash")
.navigate("Trash")
.wait(500)
.isVisibleByText("Test")
.run();
});
it("Permanently remove a note from trash", async () => {
await TestBuilder.create()
.prepare()
.createNote("Test", "Test note item")
.tapById(notesnook.listitem.menu)
.tapById("icon-trash")
.navigate("Trash")
.wait(500)
.isVisibleByText("Test")
.tapById("trash-clear")
.tapByText("Clear")
.wait(100)
.isNotVisibleByText("Test")
.navigate("Notes")
.isNotVisibleByText("Test")
.run();
});
it("Clear all notes from side menu", async () => {
await TestBuilder.create()
.prepare()
.createNote("Test", "Test note item")
.tapById(notesnook.listitem.menu)
.tapById("icon-trash")
.navigate("Trash")
.wait(500)
.isVisibleByText("Test")
.openSideMenu()
.longPressByText("Trash")
.tapByText("Clear trash")
.wait(500)
.tapByText("Clear")
.tapByText("Trash")
.isNotVisibleByText("Test")
.run();
});
});

View File

@@ -121,7 +121,8 @@ const EXTRA_ICON_NAMES = [
"calendar-today",
"bomb",
"bomb-off",
"cancel"
"cancel",
"delete-sweep-outline"
];
const __filename = fileURLToPath(import.meta.url);

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { Locator, Page } from "@playwright/test";
import { getTestId } from "../utils";
import { ContextMenuModel } from "./context-menu.model";
import { fillItemDialog, iterateList } from "./utils";
import { confirmDialog, fillItemDialog, iterateList } from "./utils";
export class NavigationMenuModel {
protected readonly page: Page;
@@ -101,4 +101,15 @@ class NavigationItemModel {
await this.menu.open(this.locator);
await this.menu.clickOnItem("remove-shortcut");
}
async clearTrash() {
await this.menu.open(this.locator);
await this.menu.clickOnItem("clear-trash");
await confirmDialog(this.page.locator(getTestId("confirm-dialog")));
}
async getClearTrashItem() {
await this.menu.open(this.locator);
return this.menu.getItem("clear-trash");
}
}

View File

@@ -0,0 +1,49 @@
/*
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 { AppModel } from "./models/app.model";
import { expect, NOTE, test } from "./utils";
test("clear trash", async ({ page }) => {
const app = new AppModel(page);
await app.goto();
const notes = await app.goToNotes();
const note = await notes.createNote(NOTE);
await note?.contextMenu.moveToTrash();
const trash = await app.goToTrash();
let trashedNote = await trash.findItem(NOTE.title);
expect(trashedNote).toBeDefined();
const trashItem = await app.navigation.findItem("Trash");
await trashItem?.clearTrash();
trashedNote = await trash.findItem(NOTE.title);
expect(trashedNote).toBeUndefined();
});
test("clear trash option should be disabled if trash is empty", async ({
page
}) => {
const app = new AppModel(page);
await app.goto();
const trashItem = await app.navigation.findItem("Trash");
const clearTrashMenuItem = await trashItem?.getClearTrashItem();
expect(await clearTrashMenuItem?.isDisabled()).toBe(true);
});

View File

@@ -228,7 +228,8 @@ import {
mdiNoteEditOutline,
mdiArrowUp,
mdiInbox,
mdiConsoleLine
mdiConsoleLine,
mdiDeleteSweepOutline
} from "@mdi/js";
import { useTheme } from "@emotion/react";
import { Theme } from "@notesnook/theme";
@@ -584,3 +585,4 @@ export const ExpandSidebar = createIcon(mdiArrowCollapseRight);
export const HamburgerMenu = createIcon(mdiMenu);
export const ArrowUp = createIcon(mdiArrowUp);
export const Inbox = createIcon(mdiInbox);
export const ClearTrash = createIcon(mdiDeleteSweepOutline);

View File

@@ -20,7 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { forwardRef, useEffect, useRef, useState } from "react";
import { Flex, Button } from "@theme-ui/components";
import { SxProp } from "@theme-ui/core";
import { Plus } from "../icons";
import { Plus, Icon } from "../icons";
import {
useStore as useSelectionStore,
store as selectionStore
@@ -76,6 +76,7 @@ type ListContainerProps = {
isSearching?: boolean;
onDrop?: (e: React.DragEvent<HTMLDivElement>) => void;
button?: {
Icon?: Icon;
onClick: () => void;
};
Scroller?: Components["Scroller"];
@@ -275,7 +276,11 @@ function ListContainer(props: ListContainerProps) {
height: 45
}}
>
<Plus color="accentForeground" />
{button.Icon ? (
<button.Icon color="accentForeground" />
) : (
<Plus color="accentForeground" />
)}
</Button>
)}
</Flex>

View File

@@ -47,7 +47,8 @@ import {
Notebook as NotebookIcon,
Plus,
SortBy,
Tag as TagIcon
Tag as TagIcon,
ClearTrash
} from "../icons";
import { SortableNavigationItem } from "./navigation-item";
import {
@@ -93,6 +94,8 @@ import { Color, Notebook, Tag } from "@notesnook/core";
import { handleDrop } from "../../common/drop-handler";
import { Menu, useMenuStore, useMenuTrigger } from "../../hooks/use-menu";
import { RenameColorDialog } from "../../dialogs/item-dialog";
import { ConfirmDialog } from "../../dialogs/confirm";
import { showToast } from "../../utils/toast";
import { strings } from "@notesnook/intl";
import Tags from "../../views/tags";
import { Notebooks } from "../../views/notebooks";
@@ -515,6 +518,7 @@ function RouteItem({
context?: { isCollapsed: boolean; collapse: () => void };
}) {
const [location] = useLocation();
const trash = useTrashStore((store) => store.trash);
return (
<SortableNavigationItem
@@ -545,6 +549,39 @@ function RouteItem({
context?.collapse();
}}
menuItems={[
...(item.id === "trash"
? [
{
type: "button" as const,
key: "clear-trash",
title: strings.clearTrash(),
isDisabled: !trash || trash.length === 0,
icon: ClearTrash.path,
onClick: async () => {
const ok = await ConfirmDialog.show({
title: strings.clearTrash(),
positiveButtonText: strings.clear(),
negativeButtonText: strings.cancel(),
message: strings.clearTrashDesc()
});
if (!ok) return;
try {
await useTrashStore.getState().clear();
showToast("success", strings.trashCleared());
} catch (e) {
if (e instanceof Error)
showToast(
"error",
`${strings.couldNotClearTrash()} ${strings.error()}: ${
e.message
}`
);
}
}
}
]
: []),
{
type: "lazy-loader",
key: "sidebar-items-loader",

View File

@@ -27,6 +27,7 @@ import { db } from "../common/db";
import { ListLoader } from "../components/loaders/list-loader";
import { ConfirmDialog } from "../dialogs/confirm";
import { strings } from "@notesnook/intl";
import { ClearTrash } from "../components/icons";
function Trash() {
useNavigate("trash", store.refresh);
@@ -47,13 +48,13 @@ function Trash() {
placeholder={<Placeholder context={filteredItems ? "search" : "trash"} />}
items={filteredItems || items}
button={{
Icon: ClearTrash,
onClick: function () {
ConfirmDialog.show({
title: strings.clearTrash(),
subtitle: strings.clearTrashDesc(),
positiveButtonText: strings.clear(),
negativeButtonText: strings.cancel(),
message: strings.areYouSure()
message: strings.clearTrashDesc()
}).then(async (res) => {
if (res) {
try {

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -2225,7 +2225,8 @@ Use this if changes from other devices are not appearing on this device. This wi
goBackToNotebooks: () => t`Go back to notebooks`,
goBackToTags: () => t`Go back to tags`,
okay: () => t`Okay`,
clearTrashDesc: () => t`Do you want to clear the trash?`,
clearTrashDesc: () =>
t`Clearing trash will permanently delete all the items in your trash. This action is IRREVERSIBLE.`,
createdAt: () => t`Created at`,
lastEditedAt: () => t`Last edited at`,
enter6DigitCode: () =>