From 765fcafc38d972a7200da646ab914a5f69e8dcfe Mon Sep 17 00:00:00 2001 From: Abdullah Atta Date: Fri, 10 Mar 2023 18:23:00 +0500 Subject: [PATCH] web: fix all tests & improve notebook/topic deletion --- apps/web/__e2e__/models/app.model.ts | 4 +- apps/web/__e2e__/models/base-view.model.ts | 28 ++- apps/web/__e2e__/models/item.model.ts | 14 +- apps/web/__e2e__/models/items-view.model.ts | 8 +- .../web/__e2e__/models/notebook-item.model.ts | 13 +- .../__e2e__/models/notebooks-view.model.ts | 2 +- apps/web/__e2e__/models/notes-view.model.ts | 9 +- .../__e2e__/models/reminders-view.model.ts | 2 +- apps/web/__e2e__/models/search-view-model.ts | 4 +- apps/web/__e2e__/models/trash-view.model.ts | 2 +- apps/web/__e2e__/models/types.ts | 2 +- apps/web/__e2e__/notebooks.test.ts | 36 +++- apps/web/__e2e__/notes.test.ts | 4 +- apps/web/__e2e__/topics.test.ts | 49 ++--- apps/web/src/assets/note2.svg | 1 + apps/web/src/common/dialog-controller.tsx | 165 +++++----------- apps/web/src/components/dialogs/confirm.js | 52 ----- apps/web/src/components/dialogs/confirm.tsx | 120 ++++++++++++ .../src/components/dialogs/issue-dialog.tsx | 37 +--- .../components/dialogs/move-note-dialog.tsx | 8 +- apps/web/src/components/group-header/index.js | 182 +++++++++--------- .../src/components/list-container/index.tsx | 3 +- apps/web/src/components/note/index.js | 8 +- apps/web/src/components/notebook/index.js | 38 ++-- apps/web/src/components/reminder/index.tsx | 16 +- apps/web/src/components/topic/index.js | 36 ++-- apps/web/src/hooks/use-menu.js | 7 + apps/web/src/navigation/routes.js | 2 +- apps/web/src/stores/notebook-store.js | 15 +- apps/web/src/utils/changelog.ts | 34 +--- apps/web/src/utils/md.ts | 54 ++++++ apps/web/src/views/auth.tsx | 1 + apps/web/src/views/topics.js | 79 +++++--- apps/web/src/views/trash.js | 17 +- 34 files changed, 575 insertions(+), 477 deletions(-) create mode 100644 apps/web/src/assets/note2.svg delete mode 100644 apps/web/src/components/dialogs/confirm.js create mode 100644 apps/web/src/components/dialogs/confirm.tsx create mode 100644 apps/web/src/utils/md.ts diff --git a/apps/web/__e2e__/models/app.model.ts b/apps/web/__e2e__/models/app.model.ts index dc8283de8..92c86f37e 100644 --- a/apps/web/__e2e__/models/app.model.ts +++ b/apps/web/__e2e__/models/app.model.ts @@ -132,7 +132,7 @@ export class AppModel { .waitFor({ state: "visible" }); } - async search(query: string) { + async search(query: string, type: string) { const searchinput = this.page.locator(getTestId("search-input")); const searchButton = this.page.locator(getTestId("search-button")); const openSearch = this.page.locator(getTestId("open-search")); @@ -140,6 +140,6 @@ export class AppModel { await openSearch.click(); await searchinput.fill(query); await searchButton.click(); - return new SearchViewModel(this.page); + return new SearchViewModel(this.page, type); } } diff --git a/apps/web/__e2e__/models/base-view.model.ts b/apps/web/__e2e__/models/base-view.model.ts index 4efacd346..676a0f3b6 100644 --- a/apps/web/__e2e__/models/base-view.model.ts +++ b/apps/web/__e2e__/models/base-view.model.ts @@ -29,14 +29,17 @@ export class BaseViewModel { private readonly listPlaceholder: Locator; private readonly sortByButton: Locator; - constructor(page: Page, pageId: string) { + constructor(page: Page, pageId: string, listType: string) { this.page = page; - this.list = page.locator(`#${pageId} >> ${getTestId("note-list")}`); + this.list = page.locator(`#${pageId} >> ${getTestId(`${listType}-list`)}`); this.listPlaceholder = page.locator( `#${pageId} >> ${getTestId("list-placeholder")}` ); - this.sortByButton = this.list.locator(getTestId("sort-icon-button")); + this.sortByButton = this.page.locator( + // TODO: + getTestId(`${pageId === "notebook" ? "notes" : pageId}-sort-button`) + ); } async findGroup(groupName: string) { @@ -103,13 +106,15 @@ export class BaseViewModel { async sort(sort: SortOptions) { const contextMenu: ContextMenuModel = new ContextMenuModel(this.page); - await contextMenu.open(this.sortByButton, "left"); - await contextMenu.clickOnItem("groupBy"); - if (!(await contextMenu.hasItem(sort.groupBy))) { - await contextMenu.close(); - return false; + if (sort.groupBy) { + await contextMenu.open(this.sortByButton, "left"); + await contextMenu.clickOnItem("groupBy"); + if (!(await contextMenu.hasItem(sort.groupBy))) { + await contextMenu.close(); + return false; + } + await contextMenu.clickOnItem(sort.groupBy); } - await contextMenu.clickOnItem(sort.groupBy); await contextMenu.open(this.sortByButton, "left"); await contextMenu.clickOnItem("sortDirection"); @@ -131,7 +136,10 @@ export class BaseViewModel { } async isEmpty() { - const totalItems = await this.list.locator(getTestId("list-item")).count(); + const items = this.list.locator( + `${getTestId(`virtuoso-item-list`)} >> ${getTestId("list-item")}` + ); + const totalItems = await items.count(); return totalItems <= 0; } } diff --git a/apps/web/__e2e__/models/item.model.ts b/apps/web/__e2e__/models/item.model.ts index dd51608e4..3b9371abd 100644 --- a/apps/web/__e2e__/models/item.model.ts +++ b/apps/web/__e2e__/models/item.model.ts @@ -22,18 +22,21 @@ import { BaseItemModel } from "./base-item.model"; import { ContextMenuModel } from "./context-menu.model"; import { NotesViewModel } from "./notes-view.model"; import { Item } from "./types"; -import { confirmDialog, denyDialog, fillItemDialog } from "./utils"; +import { confirmDialog, fillItemDialog } from "./utils"; export class ItemModel extends BaseItemModel { private readonly contextMenu: ContextMenuModel; - constructor(locator: Locator) { + constructor(locator: Locator, private readonly id: "topic" | "tag") { super(locator); this.contextMenu = new ContextMenuModel(this.page); } async open() { await this.locator.click(); - return new NotesViewModel(this.page, "notes"); + return new NotesViewModel( + this.page, + this.id === "topic" ? "notebook" : "notes" + ); } async delete() { @@ -47,9 +50,10 @@ export class ItemModel extends BaseItemModel { await this.contextMenu.open(this.locator); await this.contextMenu.clickOnItem("delete"); - if (deleteContainedNotes) await confirmDialog(this.page); - else await denyDialog(this.page); + if (deleteContainedNotes) + await this.page.locator("#deleteContainingNotes").check({ force: true }); + await confirmDialog(this.page); await this.waitFor("detached"); } diff --git a/apps/web/__e2e__/models/items-view.model.ts b/apps/web/__e2e__/models/items-view.model.ts index 071899a83..91c696472 100644 --- a/apps/web/__e2e__/models/items-view.model.ts +++ b/apps/web/__e2e__/models/items-view.model.ts @@ -28,7 +28,7 @@ export class ItemsViewModel extends BaseViewModel { private readonly createButton: Locator; constructor(page: Page, private readonly id: "topics" | "tags") { - super(page, id); + super(page, id, id); this.createButton = page.locator(getTestId(`${id}-action-button`)); } @@ -45,7 +45,11 @@ export class ItemsViewModel extends BaseViewModel { async findItem(item: Item) { const titleToCompare = this.id === "tags" ? `#${item.title}` : item.title; for await (const _item of this.iterateItems()) { - const itemModel = new ItemModel(_item); + const itemModel = new ItemModel( + _item, + // TODO: + this.id === "topics" ? "topic" : "tag" + ); const title = await itemModel.getTitle(); if (title === titleToCompare) return itemModel; } diff --git a/apps/web/__e2e__/models/notebook-item.model.ts b/apps/web/__e2e__/models/notebook-item.model.ts index 82d3cc305..288d2b7eb 100644 --- a/apps/web/__e2e__/models/notebook-item.model.ts +++ b/apps/web/__e2e__/models/notebook-item.model.ts @@ -23,7 +23,8 @@ import { ContextMenuModel } from "./context-menu.model"; import { ToggleModel } from "./toggle.model"; import { ItemsViewModel } from "./items-view.model"; import { Notebook } from "./types"; -import { confirmDialog, denyDialog, fillNotebookDialog } from "./utils"; +import { confirmDialog, fillNotebookDialog } from "./utils"; +import { NotesViewModel } from "./notes-view.model"; export class NotebookItemModel extends BaseItemModel { private readonly contextMenu: ContextMenuModel; @@ -34,7 +35,10 @@ export class NotebookItemModel extends BaseItemModel { async openNotebook() { await this.locator.click(); - return new ItemsViewModel(this.page, "topics"); + return { + topics: new ItemsViewModel(this.page, "topics"), + notes: new NotesViewModel(this.page, "notebook") + }; } async editNotebook(notebook: Notebook) { @@ -48,9 +52,10 @@ export class NotebookItemModel extends BaseItemModel { await this.contextMenu.open(this.locator); await this.contextMenu.clickOnItem("movetotrash"); - if (deleteContainedNotes) await confirmDialog(this.page); - else await denyDialog(this.page); + if (deleteContainedNotes) + await this.page.locator("#deleteContainingNotes").check({ force: true }); + await confirmDialog(this.page); await this.waitFor("detached"); } diff --git a/apps/web/__e2e__/models/notebooks-view.model.ts b/apps/web/__e2e__/models/notebooks-view.model.ts index 145841734..0367149b4 100644 --- a/apps/web/__e2e__/models/notebooks-view.model.ts +++ b/apps/web/__e2e__/models/notebooks-view.model.ts @@ -28,7 +28,7 @@ export class NotebooksViewModel extends BaseViewModel { private readonly createButton: Locator; constructor(page: Page) { - super(page, "notebooks"); + super(page, "notebooks", "notebooks"); this.createButton = page .locator(getTestId("notebooks-action-button")) .first(); diff --git a/apps/web/__e2e__/models/notes-view.model.ts b/apps/web/__e2e__/models/notes-view.model.ts index ae90ba1f3..9fc671fc1 100644 --- a/apps/web/__e2e__/models/notes-view.model.ts +++ b/apps/web/__e2e__/models/notes-view.model.ts @@ -32,9 +32,12 @@ export class NotesViewModel extends BaseViewModel { private readonly createButton: Locator; readonly editor: EditorModel; - constructor(page: Page, pageId: "home" | "notes") { - super(page, pageId); - this.createButton = page.locator(getTestId("notes-action-button")); + constructor(page: Page, pageId: "home" | "notes" | "notebook") { + super(page, pageId, pageId === "home" ? "home" : "notes"); + this.createButton = page.locator( + // TODO: + getTestId(`${pageId === "notebook" ? "notebook" : "notes"}-action-button`) + ); this.editor = new EditorModel(page); } diff --git a/apps/web/__e2e__/models/reminders-view.model.ts b/apps/web/__e2e__/models/reminders-view.model.ts index 61a61704a..4585b7abf 100644 --- a/apps/web/__e2e__/models/reminders-view.model.ts +++ b/apps/web/__e2e__/models/reminders-view.model.ts @@ -28,7 +28,7 @@ export class RemindersViewModel extends BaseViewModel { private readonly createButton: Locator; constructor(page: Page) { - super(page, "reminders"); + super(page, "reminders", "reminders"); this.createButton = page .locator(getTestId("reminders-action-button")) .first(); diff --git a/apps/web/__e2e__/models/search-view-model.ts b/apps/web/__e2e__/models/search-view-model.ts index c50576803..120cc5617 100644 --- a/apps/web/__e2e__/models/search-view-model.ts +++ b/apps/web/__e2e__/models/search-view-model.ts @@ -23,8 +23,8 @@ import { ItemModel } from "./item.model"; import { Item } from "./types"; export class SearchViewModel extends BaseViewModel { - constructor(page: Page) { - super(page, "general"); + constructor(page: Page, type: string) { + super(page, "general", type); } async findItem(item: Item) { diff --git a/apps/web/__e2e__/models/trash-view.model.ts b/apps/web/__e2e__/models/trash-view.model.ts index ab7be6eaf..06388e0a8 100644 --- a/apps/web/__e2e__/models/trash-view.model.ts +++ b/apps/web/__e2e__/models/trash-view.model.ts @@ -24,7 +24,7 @@ import { TrashItemModel } from "./trash-item.model"; export class TrashViewModel extends BaseViewModel { constructor(page: Page) { - super(page, "trash"); + super(page, "trash", "trash"); } async findItem(title: string) { diff --git a/apps/web/__e2e__/models/types.ts b/apps/web/__e2e__/models/types.ts index 355e9e3a0..eb8520681 100644 --- a/apps/web/__e2e__/models/types.ts +++ b/apps/web/__e2e__/models/types.ts @@ -45,7 +45,7 @@ export type GroupByOptions = | "week"; export type SortOptions = { - groupBy: GroupByOptions; + groupBy?: GroupByOptions; sortBy: SortByOptions; orderBy: OrderByOptions; }; diff --git a/apps/web/__e2e__/notebooks.test.ts b/apps/web/__e2e__/notebooks.test.ts index c4c460035..353b1047e 100644 --- a/apps/web/__e2e__/notebooks.test.ts +++ b/apps/web/__e2e__/notebooks.test.ts @@ -43,7 +43,19 @@ test("create a note inside a notebook", async ({ page }) => { await app.goto(); const notebooks = await app.goToNotebooks(); const notebook = await notebooks.createNotebook(NOTEBOOK); - const topics = await notebook?.openNotebook(); + const { notes } = (await notebook?.openNotebook()) || {}; + + const note = await notes?.createNote(NOTE); + + expect(note).toBeDefined(); +}); + +test("create a note inside a topic", async ({ page }) => { + const app = new AppModel(page); + await app.goto(); + const notebooks = await app.goToNotebooks(); + const notebook = await notebooks.createNotebook(NOTEBOOK); + const { topics } = (await notebook?.openNotebook()) || {}; const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] }); const notes = await topic?.open(); @@ -170,7 +182,27 @@ test("delete all notes within a notebook", async ({ page }) => { await app.goto(); const notebooks = await app.goToNotebooks(); const notebook = await notebooks.createNotebook(NOTEBOOK); - const topics = await notebook?.openNotebook(); + let { notes } = (await notebook?.openNotebook()) || {}; + for (let i = 0; i < 2; ++i) { + await notes?.createNote({ + title: `Note ${i}`, + content: NOTE.content + }); + } + await app.goBack(); + + await notebook?.moveToTrash(true); + + notes = await app.goToNotes(); + expect(await notes.isEmpty()).toBe(true); +}); + +test("delete all notes within a topic", async ({ page }) => { + const app = new AppModel(page); + await app.goto(); + const notebooks = await app.goToNotebooks(); + const notebook = await notebooks.createNotebook(NOTEBOOK); + const { topics } = (await notebook?.openNotebook()) || {}; const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] }); let notes = await topic?.open(); for (let i = 0; i < 2; ++i) { diff --git a/apps/web/__e2e__/notes.test.ts b/apps/web/__e2e__/notes.test.ts index 7aa5fe7d4..3d6d8c2fc 100644 --- a/apps/web/__e2e__/notes.test.ts +++ b/apps/web/__e2e__/notes.test.ts @@ -96,9 +96,7 @@ test("add a note to notebook", async ({ page }) => { }); expect( - await app.toasts.waitForToast( - "1 note added to 4 topics & removed from 0 topics." - ) + await app.toasts.waitForToast("1 note added to Hello and 3 others.") ).toBe(true); }); diff --git a/apps/web/__e2e__/topics.test.ts b/apps/web/__e2e__/topics.test.ts index 7b30ff6eb..9fdeb2f6b 100644 --- a/apps/web/__e2e__/topics.test.ts +++ b/apps/web/__e2e__/topics.test.ts @@ -20,20 +20,14 @@ along with this program. If not, see . import { test, expect } from "@playwright/test"; import { AppModel } from "./models/app.model"; import { Item } from "./models/types"; -import { - groupByOptions, - NOTEBOOK, - sortByOptions, - orderByOptions, - NOTE -} from "./utils"; +import { NOTEBOOK, sortByOptions, orderByOptions, NOTE } from "./utils"; test("create shortcut of a topic", async ({ page }) => { const app = new AppModel(page); await app.goto(); const notebooks = await app.goToNotebooks(); const notebook = await notebooks.createNotebook(NOTEBOOK); - const topics = await notebook?.openNotebook(); + const { topics } = (await notebook?.openNotebook()) || {}; const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] }); await topic?.createShortcut(); @@ -48,7 +42,7 @@ test("remove shortcut of a topic", async ({ page }) => { await app.goto(); const notebooks = await app.goToNotebooks(); const notebook = await notebooks.createNotebook(NOTEBOOK); - const topics = await notebook?.openNotebook(); + const { topics } = (await notebook?.openNotebook()) || {}; const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] }); await topic?.createShortcut(); @@ -64,7 +58,7 @@ test("delete a topic", async ({ page }) => { await app.goto(); const notebooks = await app.goToNotebooks(); const notebook = await notebooks.createNotebook(NOTEBOOK); - const topics = await notebook?.openNotebook(); + const { topics } = (await notebook?.openNotebook()) || {}; const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] }); await topic?.deleteWithNotes(); @@ -78,7 +72,7 @@ test("edit topics individually", async ({ page }) => { await app.goto(); const notebooks = await app.goToNotebooks(); const notebook = await notebooks.createNotebook(NOTEBOOK); - const topics = await notebook?.openNotebook(); + const { topics } = (await notebook?.openNotebook()) || {}; const editedTopics: Item[] = []; for (const title of NOTEBOOK.topics) { @@ -98,7 +92,7 @@ test("delete all notes within a topic", async ({ page }) => { await app.goto(); const notebooks = await app.goToNotebooks(); const notebook = await notebooks.createNotebook(NOTEBOOK); - const topics = await notebook?.openNotebook(); + const { topics } = (await notebook?.openNotebook()) || {}; const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] }); let notes = await topic?.open(); for (let i = 0; i < 2; ++i) { @@ -116,7 +110,7 @@ test("delete all notes within a topic", async ({ page }) => { }); test(`sort topics`, async ({ page }, info) => { - info.setTimeout(2 * 60 * 1000); + info.setTimeout(1 * 60 * 1000); const app = new AppModel(page); await app.goto(); @@ -125,27 +119,24 @@ test(`sort topics`, async ({ page }, info) => { ...NOTEBOOK, topics: ["title1", "title2", "title3", "title4", "title5"] }); - const topics = await notebook?.openNotebook(); + const { topics } = (await notebook?.openNotebook()) || {}; - for (const groupBy of groupByOptions) { - for (const sortBy of sortByOptions) { - for (const orderBy of orderByOptions) { - await test.step(`group by ${groupBy}, sort by ${sortBy}, order by ${orderBy}`, async () => { - const sortResult = await topics?.sort({ - groupBy, - orderBy, - sortBy - }); - if (!sortResult) return; - - expect(await topics?.isEmpty()).toBeFalsy(); + for (const sortBy of sortByOptions) { + for (const orderBy of orderByOptions) { + await test.step(`sort by ${sortBy}, order by ${orderBy}`, async () => { + const sortResult = await topics?.sort({ + orderBy, + sortBy }); - } + if (!sortResult) return; + + expect(await topics?.isEmpty()).toBeFalsy(); + }); } } }); -test("search topics", async ({ page }) => { +test.skip("search topics", async ({ page }) => { const app = new AppModel(page); await app.goto(); const notebooks = await app.goToNotebooks(); @@ -155,7 +146,7 @@ test("search topics", async ({ page }) => { }); await notebook?.openNotebook(); - const search = await app.search("1"); + const search = await app.search("1", "topics"); const topic = await search?.findItem({ title: "title1" }); expect((await topic?.getTitle()) === "title1").toBeTruthy(); diff --git a/apps/web/src/assets/note2.svg b/apps/web/src/assets/note2.svg new file mode 100644 index 000000000..f1c1fa674 --- /dev/null +++ b/apps/web/src/assets/note2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/apps/web/src/common/dialog-controller.tsx b/apps/web/src/common/dialog-controller.tsx index cfe7a5d3f..7e37a145f 100644 --- a/apps/web/src/common/dialog-controller.tsx +++ b/apps/web/src/common/dialog-controller.tsx @@ -19,7 +19,6 @@ along with this program. If not, see . import ReactDOM from "react-dom"; import { Dialogs } from "../components/dialogs"; -import { hardNavigate } from "../navigation"; import ThemeProvider from "../components/theme-provider"; import qclone from "qclone"; import { store as notebookStore } from "../stores/notebook-store"; @@ -29,7 +28,7 @@ import { store as editorStore } from "../stores/editor-store"; import { store as noteStore } from "../stores/note-store"; import { db } from "./db"; import { showToast } from "../utils/toast"; -import { Flex, Text } from "@theme-ui/components"; +import { Text } from "@theme-ui/components"; import * as Icon from "../components/icons"; import Config from "../utils/config"; import { formatDate } from "@notesnook/core/utils/date"; @@ -41,10 +40,11 @@ import { FeatureKeys } from "../components/dialogs/feature-dialog"; import { AuthenticatorType } from "../components/dialogs/mfa/types"; import { Suspense } from "react"; import { Reminder } from "@notesnook/core/collections/reminders"; +import { ConfirmDialogProps } from "../components/dialogs/confirm"; type DialogTypes = typeof Dialogs; type DialogIds = keyof DialogTypes; -export type Perform = (result: boolean) => void; +export type Perform = (result: T) => void; type RenderDialog = ( dialog: DialogTypes[TId], perform: (result: TReturnType) => void @@ -154,26 +154,15 @@ export function showBuyDialog(plan?: Period, couponCode?: string) { )); } -type ConfirmDialogProps = { - title?: string; - subtitle?: string; - message?: string | JSX.Element; - yesText?: string; - noText?: string; - yesAction?: () => void; - width?: string; -}; -export function confirm(props: ConfirmDialogProps) { - return showDialog("Confirm", (Dialog, perform) => ( - perform(false)} - onYes={() => { - if (props.yesAction) props.yesAction(); - perform(true); - }} - /> - )); +export function confirm( + props: Omit, "onClose"> +) { + return showDialog<"Confirm", false | Record>( + "Confirm", + (Dialog, perform) => ( + perform(result)} /> + ) + ); } export function showPromptDialog(props: { @@ -214,41 +203,26 @@ export function showToolbarConfigDialog() { } export function showError(title: string, message: string) { - return confirm({ title, message, yesText: "Okay" }); + return confirm({ title, message, positiveButtonText: "Okay" }); } export function showMultiDeleteConfirmation(length: number) { return confirm({ title: `Delete ${length} items?`, - message: ( - - These items will be{" "} - - kept in your Trash for 7 days - {" "} - after which they will be permanently removed. - - ), - yesText: `Delete selected`, - noText: "Cancel" + message: + "These items will be **kept in your Trash for 7 days** after which they will be permanently deleted.", + positiveButtonText: "Yes", + negativeButtonText: "No" }); } export function showMultiPermanentDeleteConfirmation(length: number) { return confirm({ title: `Permanently delete ${length} items?`, - message: ( - - These items will be{" "} - - permanently deleted - - {". "} - This action is IRREVERSIBLE. - - ), - yesText: `Permanently delete selected`, - noText: "Cancel" + message: + "These items will be **permanently deleted**. This is IRREVERSIBLE.", + positiveButtonText: "Yes", + negativeButtonText: "No" }); } @@ -257,8 +231,8 @@ export function showLogoutConfirmation() { title: `Logout?`, message: "Logging out will delete all local data and reset the app. Make sure you have synced your data before logging out.", - yesText: `Yes`, - noText: "No" + positiveButtonText: "Yes", + negativeButtonText: "No" }); } @@ -267,8 +241,8 @@ export function showClearSessionsConfirmation() { title: `Logout from other devices?`, message: "All other logged-in devices will be forced to logout stopping sync. Use with care lest you lose important notes.", - yesText: `Yes`, - noText: "No" + positiveButtonText: "Yes", + negativeButtonText: "No" }); } @@ -276,9 +250,7 @@ export function showAccountLoggedOutNotice(reason?: string) { return confirm({ title: "You were logged out", message: reason, - noText: "Okay", - yesText: `Relogin`, - yesAction: () => hardNavigate("/login") + negativeButtonText: "Okay" }); } @@ -287,24 +259,13 @@ export function showAppUpdatedNotice( ) { return confirm({ title: `Welcome to v${version.formatted}`, - message: ( - - Changelog: - - {version.changelog || "No change log."} - - - ), - yesText: `Yay!` + message: `## Changelog: + +\`\`\` +${version.changelog || "No change log."} +\`\`\` +`, + positiveButtonText: `Continue` }); } @@ -706,32 +667,29 @@ export function showOnboardingDialog(type: string) { )); } -export function showInvalidSystemTimeDialog({ +export async function showInvalidSystemTimeDialog({ serverTime, localTime }: { serverTime: number; localTime: number; }) { - return confirm({ + const result = await confirm({ title: "Your system clock is out of sync", subtitle: "Please correct your system date & time and reload the app to avoid syncing issues.", - message: ( - <> - Server time:{" "} - {formatDate(serverTime, { dateStyle: "medium", timeStyle: "medium" })} -
- Local time:{" "} - {formatDate(localTime, { dateStyle: "medium", timeStyle: "medium" })} -
- Please sync your system time with{" "} - https://time.is/. - - ), - yesText: "Reload app", - yesAction: () => window.location.reload() + message: `Server time: ${formatDate(serverTime, { + dateStyle: "medium", + timeStyle: "medium" + })} +Local time: ${formatDate(localTime, { + dateStyle: "medium", + timeStyle: "medium" + })} +Please sync your system time with [https://time.is](https://time.is).`, + positiveButtonText: "Reload app" }); + if (result) window.location.reload(); } export async function showUpdateAvailableNotice({ @@ -771,37 +729,18 @@ type UpdateDialogProps = { onClick: () => void; }; }; -function showUpdateDialog({ +async function showUpdateDialog({ title, subtitle, changelog, action }: UpdateDialogProps) { - return confirm({ + const result = await confirm({ title, subtitle, - message: changelog && ( - - - - ), - width: "500px", - yesText: action.text, - yesAction: action.onClick + message: changelog, + width: 500, + positiveButtonText: action.text }); + if (result && action.onClick) action.onClick(); } diff --git a/apps/web/src/components/dialogs/confirm.js b/apps/web/src/components/dialogs/confirm.js deleted file mode 100644 index fbb6b97ac..000000000 --- a/apps/web/src/components/dialogs/confirm.js +++ /dev/null @@ -1,52 +0,0 @@ -/* -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 . -*/ - -import { Box, Text } from "@theme-ui/components"; -import Dialog from "./dialog"; - -function Confirm(props) { - return ( - props.onNo(false)} - positiveButton={ - props.yesText && { - text: props.yesText, - onClick: () => props.onYes(true), - autoFocus: !!props.yesText - } - } - negativeButton={ - props.noText && { text: props.noText, onClick: () => props.onNo(false) } - } - > - - - {props.message} - - - - ); -} - -export default Confirm; diff --git a/apps/web/src/components/dialogs/confirm.tsx b/apps/web/src/components/dialogs/confirm.tsx new file mode 100644 index 000000000..d60f1dc91 --- /dev/null +++ b/apps/web/src/components/dialogs/confirm.tsx @@ -0,0 +1,120 @@ +/* +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 . +*/ + +import { Box, Checkbox, Label, Text } from "@theme-ui/components"; +import { useRef } from "react"; +import { Perform } from "../../common/dialog-controller"; +import { mdToHtml } from "../../utils/md"; +import Dialog from "./dialog"; + +type Check = { text: string; default?: boolean }; +export type ConfirmDialogProps = { + title: string; + subtitle?: string; + onClose: Perform>; + width?: number; + positiveButtonText?: string; + negativeButtonText?: string; + message?: string; + checks?: Partial>; +}; + +function ConfirmDialog( + props: ConfirmDialogProps +) { + const { + onClose, + title, + subtitle, + width, + negativeButtonText, + positiveButtonText, + message, + checks + } = props; + const checkedItems = useRef>({} as any); + + return ( + onClose(false)} + positiveButton={ + positiveButtonText + ? { + text: positiveButtonText, + onClick: () => onClose(checkedItems.current), + autoFocus: !!positiveButtonText + } + : undefined + } + negativeButton={ + negativeButtonText + ? { + text: negativeButtonText, + onClick: () => onClose(false) + } + : undefined + } + > + + {message ? ( + + ) : null} + {checks + ? Object.entries(checks).map( + ([id, check]) => + check && ( + + ) + ) + : null} + + + ); +} + +export default ConfirmDialog; diff --git a/apps/web/src/components/dialogs/issue-dialog.tsx b/apps/web/src/components/dialogs/issue-dialog.tsx index 7eddd259f..6c6a44d13 100644 --- a/apps/web/src/components/dialogs/issue-dialog.tsx +++ b/apps/web/src/components/dialogs/issue-dialog.tsx @@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { Flex, Link, Text } from "@theme-ui/components"; +import { Flex, Text } from "@theme-ui/components"; import { appVersion } from "../../utils/version"; import Field from "../field"; import Dialog from "./dialog"; @@ -152,33 +152,14 @@ export default IssueDialog; function showIssueReportedDialog({ url }: { url: string }) { return confirm({ title: "Thank you for reporting!", - yesAction: () => clipboard.writeText(url), - yesText: "Copy link", - message: ( - <> -

- You can track your bug report at{" "} - - {url} - - . -

-

- Please note that we will respond to your bug report on the link above.{" "} - - We recommended that you save the above link for later reference. - -

-

- If your issue is critical (e.g. notes not syncing, crashes etc.), - please{" "} - - join our Discord community - {" "} - for one-to-one support. -

- - ) + positiveButtonText: "Copy link", + message: `You can track your bug report at [${url}](${url}). + + Please note that we will respond to your bug report on the link above. **We recommended that you save the above link for later reference.** + + If your issue is critical (e.g. notes not syncing, crashes etc.), please [join our Discord community](https://discord.com/invite/zQBK97EE22) for one-to-one support.` + }).then((result) => { + result && clipboard.writeText(url); }); } diff --git a/apps/web/src/components/dialogs/move-note-dialog.tsx b/apps/web/src/components/dialogs/move-note-dialog.tsx index 46b715df6..440fbd05c 100644 --- a/apps/web/src/components/dialogs/move-note-dialog.tsx +++ b/apps/web/src/components/dialogs/move-note-dialog.tsx @@ -145,7 +145,9 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) { if (stringified) { showToast( "success", - stringified.replace("Add", "Added").replace("remove", "removed") + `${pluralize(noteIds.length, "note", "notes")} ${stringified + .replace("Add", "added") + .replace("remove", "removed")}` ); } @@ -635,12 +637,12 @@ function stringifySelected(suggestion: NotebookReference[]) { if (added.length > 1) parts.push(`and ${added.length - 1} others`); if (removed.length >= 1) { - parts.push("remove from"); + parts.push("& remove from"); parts.push(removed[0]); } if (removed.length > 1) parts.push(`and ${removed.length - 1} others`); - return parts.join(" "); + return parts.join(" ") + "."; } function resolve(ref: NotebookReference) { diff --git a/apps/web/src/components/group-header/index.js b/apps/web/src/components/group-header/index.js index e4d124390..3c113d99e 100644 --- a/apps/web/src/components/group-header/index.js +++ b/apps/web/src/components/group-header/index.js @@ -21,7 +21,7 @@ import * as Icon from "../icons"; import { useEffect, useMemo, useRef, useState } from "react"; import { Button, Flex, Text } from "@theme-ui/components"; import { db } from "../../common/db"; -import { useMenuTrigger } from "../../hooks/use-menu"; +import { Menu, useMenuTrigger } from "../../hooks/use-menu"; import { useStore as useNoteStore } from "../../stores/note-store"; import { useStore as useNotebookStore } from "../../stores/notebook-store"; import useMobile from "../../hooks/use-mobile"; @@ -36,84 +36,97 @@ const groupByToTitleMap = { month: "Month" }; -const menuItems = [ - { - key: "sortDirection", - title: "Order by", - icon: ({ groupOptions }) => - groupOptions.sortDirection === "asc" - ? groupOptions.sortBy === "title" - ? Icon.OrderAtoZ - : Icon.OrderOldestNewest - : groupOptions.sortBy === "title" - ? Icon.OrderZtoA - : Icon.OrderNewestOldest, - items: map([ - { - key: "asc", - title: ({ groupOptions }) => - groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest" - }, - { - key: "desc", - title: ({ groupOptions }) => - groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest" +const groupByMenu = { + key: "groupBy", + title: "Group by", + icon: Icon.GroupBy, + items: map([ + { key: "none", title: "None" }, + { key: "default", title: "Default" }, + { key: "year", title: "Year" }, + { key: "month", title: "Month" }, + { key: "week", title: "Week" }, + { key: "abc", title: "A - Z" } + ]) +}; + +const orderByMenu = { + key: "sortDirection", + title: "Order by", + icon: ({ groupOptions }) => + groupOptions.sortDirection === "asc" + ? groupOptions.sortBy === "title" + ? Icon.OrderAtoZ + : Icon.OrderOldestNewest + : groupOptions.sortBy === "title" + ? Icon.OrderZtoA + : Icon.OrderNewestOldest, + items: map([ + { + key: "asc", + title: ({ groupOptions }) => + groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest" + }, + { + key: "desc", + title: ({ groupOptions }) => + groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest" + } + ]) +}; + +const sortByMenu = { + key: "sortBy", + title: "Sort by", + icon: Icon.SortBy, + items: map([ + { + key: "dateCreated", + title: "Date created", + hidden: ({ type }) => type === "trash" + }, + { + key: "dateEdited", + title: "Date edited", + hidden: ({ type }) => type === "trash" || type === "tags" + }, + { + key: "dateDeleted", + title: "Date deleted", + hidden: ({ type }) => type !== "trash" + }, + { + key: "dateModified", + title: "Date modified", + hidden: ({ type }) => type !== "tags" + }, + { + key: "title", + title: "Title", + hidden: ({ groupOptions, parent, isUngrouped }, item) => { + if (isUngrouped) return false; + + return ( + parent?.key === "sortBy" && + item.key === "title" && + groupOptions.groupBy !== "abc" && + groupOptions.groupBy !== "none" + ); } - ]) - }, - { - key: "sortBy", - title: "Sort by", - icon: Icon.SortBy, - items: map([ - { - key: "dateCreated", - title: "Date created", - hidden: ({ type }) => type === "trash" - }, - { - key: "dateEdited", - title: "Date edited", - hidden: ({ type }) => type === "trash" || type === "tags" - }, - { - key: "dateDeleted", - title: "Date deleted", - hidden: ({ type }) => type !== "trash" - }, - { - key: "dateModified", - title: "Date modified", - hidden: ({ type }) => type !== "tags" - }, - { - key: "title", - title: "Title", - hidden: ({ groupOptions, parent }, item) => { - return ( - parent?.key === "sortBy" && - item.key === "title" && - groupOptions.groupBy !== "abc" && - groupOptions.groupBy !== "none" - ); - } - } - ]) - }, - { - key: "groupBy", - title: "Group by", - icon: Icon.GroupBy, - items: map([ - { key: "none", title: "None" }, - { key: "default", title: "Default" }, - { key: "year", title: "Year" }, - { key: "month", title: "Month" }, - { key: "week", title: "Week" }, - { key: "abc", title: "A - Z" } - ]) - } -]; + } + ]) +}; + +export function showSortMenu(type, refresh) { + const groupOptions = db.settings.getGroupOptions(type); + Menu.openMenu([orderByMenu, sortByMenu], { + title: "Sort", + groupOptions, + refresh, + type, + isUngrouped: true + }); +} function changeGroupOptions({ groupOptions, type, refresh, parent }, item) { if (!parent) return false; @@ -132,19 +145,10 @@ function isChecked({ groupOptions, parent }, item) { return groupOptions[parent.key] === item.key; } -function isDisabled({ groupOptions, parent }, item) { - return ( - parent?.key === "sortBy" && - item.key === "title" && - groupOptions.groupBy === "abc" - ); -} - function map(items) { return items.map((item) => { item.checked = isChecked; item.onClick = changeGroupOptions; - item.disabled = isDisabled; return item; }, []); } @@ -248,7 +252,7 @@ function GroupHeader(props) { {type && ( { - const phrase = items.length > 1 ? "this notebook" : "these notebooks"; - const shouldDeleteNotes = await confirm({ - title: `Delete notes in ${phrase}?`, - message: `These notes will be moved to trash and permanently deleted after 7 days.`, - yesText: `Yes`, - noText: "No" - }); - - if (shouldDeleteNotes) { - const notes = []; - for (const item of items) { - const topics = db.notebooks.notebook(item.id).topics; - for (const topic of topics.all) { - notes.push(...topics.topic(topic.id).all); + const result = await confirm({ + title: `Delete ${pluralize(items.length, "notebook", "notebooks")}?`, + positiveButtonText: `Yes`, + negativeButtonText: "No", + checks: { + deleteContainingNotes: { + text: `Delete all containing notes` } } - await Multiselect.moveNotesToTrash(notes, false); + }); + + if (result) { + if (result.deleteContainingNotes) { + const notes = []; + for (const item of items) { + notes.push(...db.relations.from(item, "note")); + const topics = db.notebooks.notebook(item.id).topics; + for (const topic of topics.all) { + notes.push(...topics.topic(topic.id).all); + } + } + await Multiselect.moveNotesToTrash(notes, false); + } + await Multiselect.moveNotebooksToTrash(items); } - await Multiselect.moveNotebooksToTrash(items); }, multiSelect: true } diff --git a/apps/web/src/components/reminder/index.tsx b/apps/web/src/components/reminder/index.tsx index 7f483fc77..f44a2b9aa 100644 --- a/apps/web/src/components/reminder/index.tsx +++ b/apps/web/src/components/reminder/index.tsx @@ -19,7 +19,7 @@ along with this program. If not, see . import React from "react"; import ListItem from "../list-item"; -import { Flex, Text } from "@theme-ui/components"; +import { Flex } from "@theme-ui/components"; import * as Icon from "../icons"; import IconTag from "../icon-tag"; import { @@ -164,15 +164,11 @@ const menuItems: MenuItem[] = [ onClick: async ({ items }) => { confirm({ title: `Delete ${pluralize(items.length, "reminder", "reminders")}`, - message: ( - - Are you sure you want to proceed? - This action is IRREVERSIBLE. - - ), - yesText: "Yes", - noText: "No", - yesAction: () => Multiselect.moveRemindersToTrash(items) + message: `Are you sure you want to proceed? **This action is IRREVERSIBLE**.`, + positiveButtonText: "Yes", + negativeButtonText: "No" + }).then((result) => { + result && Multiselect.moveRemindersToTrash(items); }); }, multiSelect: true diff --git a/apps/web/src/components/topic/index.js b/apps/web/src/components/topic/index.js index 4de13cbd7..41f4e06b3 100644 --- a/apps/web/src/components/topic/index.js +++ b/apps/web/src/components/topic/index.js @@ -27,6 +27,7 @@ import * as Icon from "../icons"; import { Multiselect } from "../../common/multi-select"; import { confirm } from "../../common/dialog-controller"; import { useStore as useNotesStore } from "../../stores/note-store"; +import { pluralize } from "../../utils/string"; function Topic({ item, index, onClick }) { const { id, notebookId } = item; @@ -36,7 +37,7 @@ function Topic({ item, index, onClick }) { ); const totalNotes = useMemo(() => { - return db.notebooks.notebook(notebookId)?.topics.topic(id).totalNotes; + return db.notebooks.notebook(notebookId)?.topics.topic(id)?.totalNotes || 0; }, [id, notebookId]); return ( @@ -94,23 +95,30 @@ const menuItems = [ color: "error", iconColor: "error", onClick: async ({ items, notebookId }) => { - const phrase = items.length > 1 ? "this topic" : "these topics"; - const shouldDeleteNotes = await confirm({ - title: `Delete notes in ${phrase}?`, - message: `These notes will be moved to trash and permanently deleted after 7 days.`, - yesText: `Yes`, - noText: "No" + const result = await confirm({ + title: `Delete ${pluralize(items.length, "topic", "topics")}?`, + positiveButtonText: `Yes`, + negativeButtonText: "No", + checks: { + deleteContainingNotes: { + text: `Delete all containing notes` + } + } }); - if (shouldDeleteNotes) { - const notes = []; - for (const item of items) { - const topic = db.notebooks.notebook(notebookId).topics.topic(item.id); - notes.push(...topic.all); + if (result) { + if (result.deleteContainingNotes) { + const notes = []; + for (const item of items) { + const topic = db.notebooks + .notebook(notebookId) + .topics.topic(item.id); + notes.push(...topic.all); + } + await Multiselect.moveNotesToTrash(notes, false); } - await Multiselect.moveNotesToTrash(notes, false); + await Multiselect.deleteTopics(notebookId, items); } - await Multiselect.deleteTopics(notebookId, items); }, multiSelect: true } diff --git a/apps/web/src/hooks/use-menu.js b/apps/web/src/hooks/use-menu.js index ff5ca44e0..f2e357280 100644 --- a/apps/web/src/hooks/use-menu.js +++ b/apps/web/src/hooks/use-menu.js @@ -62,6 +62,13 @@ export function useMenuTrigger() { }; } +export const Menu = { + openMenu: (items, data) => useMenuStore.getState().open(items, data), + closeMenu: () => useMenuStore.getState().close(), + isOpen: () => useMenuStore.getState().isOpen, + target: () => useMenuStore.getState().target +}; + export function useMenu() { const [items, data] = useMenuStore((store) => [store.items, store.data]); return { items, data }; diff --git a/apps/web/src/navigation/routes.js b/apps/web/src/navigation/routes.js index 88ab6f727..7bc738637 100644 --- a/apps/web/src/navigation/routes.js +++ b/apps/web/src/navigation/routes.js @@ -87,7 +87,7 @@ const routes = { value: { id: notebookId, topic: topicId } }); return { - key: "topic", + key: "notebook", type: "notebook", title: topic.title, component: , diff --git a/apps/web/src/stores/notebook-store.js b/apps/web/src/stores/notebook-store.js index 2b26e4e01..254dd99c5 100644 --- a/apps/web/src/stores/notebook-store.js +++ b/apps/web/src/stores/notebook-store.js @@ -27,7 +27,8 @@ import Config from "../utils/config"; class NotebookStore extends BaseStore { notebooks = []; - selectedNotebookId = 0; + selectedNotebook = undefined; + selectedNotebookTopics = []; viewMode = Config.get("notebooks:viewMode", "detailed"); setViewMode = (viewMode) => { @@ -42,7 +43,7 @@ class NotebookStore extends BaseStore { db.settings.getGroupOptions("notebooks") ); }); - this.setSelectedNotebook(this.get().selectedNotebookId); + this.setSelectedNotebook(this.get().selectedNotebook?.id); }; delete = async (...ids) => { @@ -59,8 +60,16 @@ class NotebookStore extends BaseStore { }; setSelectedNotebook = (id) => { + if (!id) return; + const notebook = db.notebooks?.notebook(id)?.data; + if (!notebook) return; + this.set((state) => { - state.selectedNotebookId = id; + state.selectedNotebook = notebook; + state.selectedNotebookTopics = groupArray( + notebook.topics, + db.settings.getGroupOptions("topics") + ); }); }; } diff --git a/apps/web/src/utils/changelog.ts b/apps/web/src/utils/changelog.ts index 8ab313aba..ebc935c89 100644 --- a/apps/web/src/utils/changelog.ts +++ b/apps/web/src/utils/changelog.ts @@ -17,38 +17,6 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . */ -import { marked } from "marked"; - -const emoji: marked.TokenizerExtension & marked.RendererExtension = { - name: "emoji", - level: "inline", - start(src) { - return src.indexOf(":"); - }, - tokenizer(src, _tokens) { - const rule = /^:(\w+):/; - const match = rule.exec(src); - if (match) { - return { - type: "emoji", - raw: match[0], - emoji: match[1] - }; - } - }, - renderer(token) { - return ``; - } -}; - -const renderer = new marked.Renderer(); -renderer.link = function (href, title, text) { - return `${text}`; -}; -marked.use({ extensions: [emoji] }); - export async function getChangelog(tag: string) { try { if (!tag) return "No changelog found."; @@ -63,7 +31,7 @@ export async function getChangelog(tag: string) { if (!release || !release.body) return "No changelog found."; const { body } = release; - return await marked.parse(body, { async: true, renderer, gfm: true }); + return body; } catch (e) { console.error(e); return "No changelog found."; diff --git a/apps/web/src/utils/md.ts b/apps/web/src/utils/md.ts new file mode 100644 index 000000000..b5894ba1a --- /dev/null +++ b/apps/web/src/utils/md.ts @@ -0,0 +1,54 @@ +/* +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 . +*/ + +import { marked } from "marked"; + +const emoji: marked.TokenizerExtension & marked.RendererExtension = { + name: "emoji", + level: "inline", + start(src) { + return src.indexOf(":"); + }, + tokenizer(src, _tokens) { + const rule = /^:(\w+):/; + const match = rule.exec(src); + if (match) { + return { + type: "emoji", + raw: match[0], + emoji: match[1] + }; + } + }, + renderer(token) { + return ``; + } +}; + +const renderer = new marked.Renderer(); +renderer.link = function (href, title, text) { + return `${text}`; +}; +marked.use({ extensions: [emoji] }); + +export function mdToHtml(markdown: string) { + return marked.parse(markdown, { async: false, renderer, gfm: true }); +} diff --git a/apps/web/src/views/auth.tsx b/apps/web/src/views/auth.tsx index 1534de29a..9a3a83d2b 100644 --- a/apps/web/src/views/auth.tsx +++ b/apps/web/src/views/auth.tsx @@ -755,6 +755,7 @@ function MFASelector(props: BaseAuthComponentProps<"mfa:select">) { (method, index) => isValidMethod(method.type) && ( + + + + refresh(selectedNotebook.id)} - items={selectedNotebook.topics} + items={topics} context={{ notebookId: selectedNotebook.id }} diff --git a/apps/web/src/views/trash.js b/apps/web/src/views/trash.js index 7cd9d7f4a..5a2d6c1e8 100644 --- a/apps/web/src/views/trash.js +++ b/apps/web/src/views/trash.js @@ -51,20 +51,9 @@ function Trash() { confirm({ title: "Clear Trash", subtitle: "Are you sure you want to clear all the trash?", - yesText: "Clear trash", - noText: "Cancel", - message: ( - <> - This action is{" "} - - IRREVERSIBLE - - . You will{" "} - - not be able to recover any of these items. - - - ) + positiveButtonText: "Clear trash", + negativeButtonText: "Cancel", + message: `Are you sure you want to proceed? **This action is IRREVERSIBLE**.` }).then(async (res) => { if (res) { try {