mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-09-01 19:49:54 +02:00
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:
@@ -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();
|
||||
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
};
|
||||
|
||||
|
||||
53
apps/mobile/e2e/tests/trash.e2e.ts
Normal file
53
apps/mobile/e2e/tests/trash.e2e.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
Binary file not shown.
@@ -121,7 +121,8 @@ const EXTRA_ICON_NAMES = [
|
||||
"calendar-today",
|
||||
"bomb",
|
||||
"bomb-off",
|
||||
"cancel"
|
||||
"cancel",
|
||||
"delete-sweep-outline"
|
||||
];
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
49
apps/web/__e2e__/trash.test.ts
Normal file
49
apps/web/__e2e__/trash.test.ts
Normal 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);
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
@@ -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: () =>
|
||||
|
||||
Reference in New Issue
Block a user