mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 15:09:33 +01:00
web: fix all tests & improve notebook/topic deletion
This commit is contained in:
committed by
Abdullah Atta
parent
7a53881eb3
commit
765fcafc38
@@ -132,7 +132,7 @@ export class AppModel {
|
|||||||
.waitFor({ state: "visible" });
|
.waitFor({ state: "visible" });
|
||||||
}
|
}
|
||||||
|
|
||||||
async search(query: string) {
|
async search(query: string, type: string) {
|
||||||
const searchinput = this.page.locator(getTestId("search-input"));
|
const searchinput = this.page.locator(getTestId("search-input"));
|
||||||
const searchButton = this.page.locator(getTestId("search-button"));
|
const searchButton = this.page.locator(getTestId("search-button"));
|
||||||
const openSearch = this.page.locator(getTestId("open-search"));
|
const openSearch = this.page.locator(getTestId("open-search"));
|
||||||
@@ -140,6 +140,6 @@ export class AppModel {
|
|||||||
await openSearch.click();
|
await openSearch.click();
|
||||||
await searchinput.fill(query);
|
await searchinput.fill(query);
|
||||||
await searchButton.click();
|
await searchButton.click();
|
||||||
return new SearchViewModel(this.page);
|
return new SearchViewModel(this.page, type);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,14 +29,17 @@ export class BaseViewModel {
|
|||||||
private readonly listPlaceholder: Locator;
|
private readonly listPlaceholder: Locator;
|
||||||
private readonly sortByButton: Locator;
|
private readonly sortByButton: Locator;
|
||||||
|
|
||||||
constructor(page: Page, pageId: string) {
|
constructor(page: Page, pageId: string, listType: string) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
this.list = page.locator(`#${pageId} >> ${getTestId("note-list")}`);
|
this.list = page.locator(`#${pageId} >> ${getTestId(`${listType}-list`)}`);
|
||||||
this.listPlaceholder = page.locator(
|
this.listPlaceholder = page.locator(
|
||||||
`#${pageId} >> ${getTestId("list-placeholder")}`
|
`#${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) {
|
async findGroup(groupName: string) {
|
||||||
@@ -103,13 +106,15 @@ export class BaseViewModel {
|
|||||||
async sort(sort: SortOptions) {
|
async sort(sort: SortOptions) {
|
||||||
const contextMenu: ContextMenuModel = new ContextMenuModel(this.page);
|
const contextMenu: ContextMenuModel = new ContextMenuModel(this.page);
|
||||||
|
|
||||||
await contextMenu.open(this.sortByButton, "left");
|
if (sort.groupBy) {
|
||||||
await contextMenu.clickOnItem("groupBy");
|
await contextMenu.open(this.sortByButton, "left");
|
||||||
if (!(await contextMenu.hasItem(sort.groupBy))) {
|
await contextMenu.clickOnItem("groupBy");
|
||||||
await contextMenu.close();
|
if (!(await contextMenu.hasItem(sort.groupBy))) {
|
||||||
return false;
|
await contextMenu.close();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await contextMenu.clickOnItem(sort.groupBy);
|
||||||
}
|
}
|
||||||
await contextMenu.clickOnItem(sort.groupBy);
|
|
||||||
|
|
||||||
await contextMenu.open(this.sortByButton, "left");
|
await contextMenu.open(this.sortByButton, "left");
|
||||||
await contextMenu.clickOnItem("sortDirection");
|
await contextMenu.clickOnItem("sortDirection");
|
||||||
@@ -131,7 +136,10 @@ export class BaseViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isEmpty() {
|
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;
|
return totalItems <= 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,18 +22,21 @@ import { BaseItemModel } from "./base-item.model";
|
|||||||
import { ContextMenuModel } from "./context-menu.model";
|
import { ContextMenuModel } from "./context-menu.model";
|
||||||
import { NotesViewModel } from "./notes-view.model";
|
import { NotesViewModel } from "./notes-view.model";
|
||||||
import { Item } from "./types";
|
import { Item } from "./types";
|
||||||
import { confirmDialog, denyDialog, fillItemDialog } from "./utils";
|
import { confirmDialog, fillItemDialog } from "./utils";
|
||||||
|
|
||||||
export class ItemModel extends BaseItemModel {
|
export class ItemModel extends BaseItemModel {
|
||||||
private readonly contextMenu: ContextMenuModel;
|
private readonly contextMenu: ContextMenuModel;
|
||||||
constructor(locator: Locator) {
|
constructor(locator: Locator, private readonly id: "topic" | "tag") {
|
||||||
super(locator);
|
super(locator);
|
||||||
this.contextMenu = new ContextMenuModel(this.page);
|
this.contextMenu = new ContextMenuModel(this.page);
|
||||||
}
|
}
|
||||||
|
|
||||||
async open() {
|
async open() {
|
||||||
await this.locator.click();
|
await this.locator.click();
|
||||||
return new NotesViewModel(this.page, "notes");
|
return new NotesViewModel(
|
||||||
|
this.page,
|
||||||
|
this.id === "topic" ? "notebook" : "notes"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete() {
|
async delete() {
|
||||||
@@ -47,9 +50,10 @@ export class ItemModel extends BaseItemModel {
|
|||||||
await this.contextMenu.open(this.locator);
|
await this.contextMenu.open(this.locator);
|
||||||
await this.contextMenu.clickOnItem("delete");
|
await this.contextMenu.clickOnItem("delete");
|
||||||
|
|
||||||
if (deleteContainedNotes) await confirmDialog(this.page);
|
if (deleteContainedNotes)
|
||||||
else await denyDialog(this.page);
|
await this.page.locator("#deleteContainingNotes").check({ force: true });
|
||||||
|
|
||||||
|
await confirmDialog(this.page);
|
||||||
await this.waitFor("detached");
|
await this.waitFor("detached");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class ItemsViewModel extends BaseViewModel {
|
|||||||
private readonly createButton: Locator;
|
private readonly createButton: Locator;
|
||||||
|
|
||||||
constructor(page: Page, private readonly id: "topics" | "tags") {
|
constructor(page: Page, private readonly id: "topics" | "tags") {
|
||||||
super(page, id);
|
super(page, id, id);
|
||||||
this.createButton = page.locator(getTestId(`${id}-action-button`));
|
this.createButton = page.locator(getTestId(`${id}-action-button`));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +45,11 @@ export class ItemsViewModel extends BaseViewModel {
|
|||||||
async findItem(item: Item) {
|
async findItem(item: Item) {
|
||||||
const titleToCompare = this.id === "tags" ? `#${item.title}` : item.title;
|
const titleToCompare = this.id === "tags" ? `#${item.title}` : item.title;
|
||||||
for await (const _item of this.iterateItems()) {
|
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();
|
const title = await itemModel.getTitle();
|
||||||
if (title === titleToCompare) return itemModel;
|
if (title === titleToCompare) return itemModel;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,8 @@ import { ContextMenuModel } from "./context-menu.model";
|
|||||||
import { ToggleModel } from "./toggle.model";
|
import { ToggleModel } from "./toggle.model";
|
||||||
import { ItemsViewModel } from "./items-view.model";
|
import { ItemsViewModel } from "./items-view.model";
|
||||||
import { Notebook } from "./types";
|
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 {
|
export class NotebookItemModel extends BaseItemModel {
|
||||||
private readonly contextMenu: ContextMenuModel;
|
private readonly contextMenu: ContextMenuModel;
|
||||||
@@ -34,7 +35,10 @@ export class NotebookItemModel extends BaseItemModel {
|
|||||||
|
|
||||||
async openNotebook() {
|
async openNotebook() {
|
||||||
await this.locator.click();
|
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) {
|
async editNotebook(notebook: Notebook) {
|
||||||
@@ -48,9 +52,10 @@ export class NotebookItemModel extends BaseItemModel {
|
|||||||
await this.contextMenu.open(this.locator);
|
await this.contextMenu.open(this.locator);
|
||||||
await this.contextMenu.clickOnItem("movetotrash");
|
await this.contextMenu.clickOnItem("movetotrash");
|
||||||
|
|
||||||
if (deleteContainedNotes) await confirmDialog(this.page);
|
if (deleteContainedNotes)
|
||||||
else await denyDialog(this.page);
|
await this.page.locator("#deleteContainingNotes").check({ force: true });
|
||||||
|
|
||||||
|
await confirmDialog(this.page);
|
||||||
await this.waitFor("detached");
|
await this.waitFor("detached");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class NotebooksViewModel extends BaseViewModel {
|
|||||||
private readonly createButton: Locator;
|
private readonly createButton: Locator;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
super(page, "notebooks");
|
super(page, "notebooks", "notebooks");
|
||||||
this.createButton = page
|
this.createButton = page
|
||||||
.locator(getTestId("notebooks-action-button"))
|
.locator(getTestId("notebooks-action-button"))
|
||||||
.first();
|
.first();
|
||||||
|
|||||||
@@ -32,9 +32,12 @@ export class NotesViewModel extends BaseViewModel {
|
|||||||
private readonly createButton: Locator;
|
private readonly createButton: Locator;
|
||||||
readonly editor: EditorModel;
|
readonly editor: EditorModel;
|
||||||
|
|
||||||
constructor(page: Page, pageId: "home" | "notes") {
|
constructor(page: Page, pageId: "home" | "notes" | "notebook") {
|
||||||
super(page, pageId);
|
super(page, pageId, pageId === "home" ? "home" : "notes");
|
||||||
this.createButton = page.locator(getTestId("notes-action-button"));
|
this.createButton = page.locator(
|
||||||
|
// TODO:
|
||||||
|
getTestId(`${pageId === "notebook" ? "notebook" : "notes"}-action-button`)
|
||||||
|
);
|
||||||
this.editor = new EditorModel(page);
|
this.editor = new EditorModel(page);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export class RemindersViewModel extends BaseViewModel {
|
|||||||
private readonly createButton: Locator;
|
private readonly createButton: Locator;
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
super(page, "reminders");
|
super(page, "reminders", "reminders");
|
||||||
this.createButton = page
|
this.createButton = page
|
||||||
.locator(getTestId("reminders-action-button"))
|
.locator(getTestId("reminders-action-button"))
|
||||||
.first();
|
.first();
|
||||||
|
|||||||
@@ -23,8 +23,8 @@ import { ItemModel } from "./item.model";
|
|||||||
import { Item } from "./types";
|
import { Item } from "./types";
|
||||||
|
|
||||||
export class SearchViewModel extends BaseViewModel {
|
export class SearchViewModel extends BaseViewModel {
|
||||||
constructor(page: Page) {
|
constructor(page: Page, type: string) {
|
||||||
super(page, "general");
|
super(page, "general", type);
|
||||||
}
|
}
|
||||||
|
|
||||||
async findItem(item: Item) {
|
async findItem(item: Item) {
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ import { TrashItemModel } from "./trash-item.model";
|
|||||||
|
|
||||||
export class TrashViewModel extends BaseViewModel {
|
export class TrashViewModel extends BaseViewModel {
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
super(page, "trash");
|
super(page, "trash", "trash");
|
||||||
}
|
}
|
||||||
|
|
||||||
async findItem(title: string) {
|
async findItem(title: string) {
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export type GroupByOptions =
|
|||||||
| "week";
|
| "week";
|
||||||
|
|
||||||
export type SortOptions = {
|
export type SortOptions = {
|
||||||
groupBy: GroupByOptions;
|
groupBy?: GroupByOptions;
|
||||||
sortBy: SortByOptions;
|
sortBy: SortByOptions;
|
||||||
orderBy: OrderByOptions;
|
orderBy: OrderByOptions;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -43,7 +43,19 @@ test("create a note inside a notebook", async ({ page }) => {
|
|||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
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 topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||||
const notes = await topic?.open();
|
const notes = await topic?.open();
|
||||||
|
|
||||||
@@ -170,7 +182,27 @@ test("delete all notes within a notebook", async ({ page }) => {
|
|||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
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] });
|
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||||
let notes = await topic?.open();
|
let notes = await topic?.open();
|
||||||
for (let i = 0; i < 2; ++i) {
|
for (let i = 0; i < 2; ++i) {
|
||||||
|
|||||||
@@ -96,9 +96,7 @@ test("add a note to notebook", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await app.toasts.waitForToast(
|
await app.toasts.waitForToast("1 note added to Hello and 3 others.")
|
||||||
"1 note added to 4 topics & removed from 0 topics."
|
|
||||||
)
|
|
||||||
).toBe(true);
|
).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -20,20 +20,14 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
import { test, expect } from "@playwright/test";
|
import { test, expect } from "@playwright/test";
|
||||||
import { AppModel } from "./models/app.model";
|
import { AppModel } from "./models/app.model";
|
||||||
import { Item } from "./models/types";
|
import { Item } from "./models/types";
|
||||||
import {
|
import { NOTEBOOK, sortByOptions, orderByOptions, NOTE } from "./utils";
|
||||||
groupByOptions,
|
|
||||||
NOTEBOOK,
|
|
||||||
sortByOptions,
|
|
||||||
orderByOptions,
|
|
||||||
NOTE
|
|
||||||
} from "./utils";
|
|
||||||
|
|
||||||
test("create shortcut of a topic", async ({ page }) => {
|
test("create shortcut of a topic", async ({ page }) => {
|
||||||
const app = new AppModel(page);
|
const app = new AppModel(page);
|
||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
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] });
|
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||||
|
|
||||||
await topic?.createShortcut();
|
await topic?.createShortcut();
|
||||||
@@ -48,7 +42,7 @@ test("remove shortcut of a topic", async ({ page }) => {
|
|||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
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] });
|
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||||
await topic?.createShortcut();
|
await topic?.createShortcut();
|
||||||
|
|
||||||
@@ -64,7 +58,7 @@ test("delete a topic", async ({ page }) => {
|
|||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
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] });
|
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||||
|
|
||||||
await topic?.deleteWithNotes();
|
await topic?.deleteWithNotes();
|
||||||
@@ -78,7 +72,7 @@ test("edit topics individually", async ({ page }) => {
|
|||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
||||||
const topics = await notebook?.openNotebook();
|
const { topics } = (await notebook?.openNotebook()) || {};
|
||||||
|
|
||||||
const editedTopics: Item[] = [];
|
const editedTopics: Item[] = [];
|
||||||
for (const title of NOTEBOOK.topics) {
|
for (const title of NOTEBOOK.topics) {
|
||||||
@@ -98,7 +92,7 @@ test("delete all notes within a topic", async ({ page }) => {
|
|||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
const notebook = await notebooks.createNotebook(NOTEBOOK);
|
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] });
|
const topic = await topics?.findItem({ title: NOTEBOOK.topics[0] });
|
||||||
let notes = await topic?.open();
|
let notes = await topic?.open();
|
||||||
for (let i = 0; i < 2; ++i) {
|
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) => {
|
test(`sort topics`, async ({ page }, info) => {
|
||||||
info.setTimeout(2 * 60 * 1000);
|
info.setTimeout(1 * 60 * 1000);
|
||||||
|
|
||||||
const app = new AppModel(page);
|
const app = new AppModel(page);
|
||||||
await app.goto();
|
await app.goto();
|
||||||
@@ -125,27 +119,24 @@ test(`sort topics`, async ({ page }, info) => {
|
|||||||
...NOTEBOOK,
|
...NOTEBOOK,
|
||||||
topics: ["title1", "title2", "title3", "title4", "title5"]
|
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 sortBy of sortByOptions) {
|
for (const orderBy of orderByOptions) {
|
||||||
for (const orderBy of orderByOptions) {
|
await test.step(`sort by ${sortBy}, order by ${orderBy}`, async () => {
|
||||||
await test.step(`group by ${groupBy}, sort by ${sortBy}, order by ${orderBy}`, async () => {
|
const sortResult = await topics?.sort({
|
||||||
const sortResult = await topics?.sort({
|
orderBy,
|
||||||
groupBy,
|
sortBy
|
||||||
orderBy,
|
|
||||||
sortBy
|
|
||||||
});
|
|
||||||
if (!sortResult) return;
|
|
||||||
|
|
||||||
expect(await topics?.isEmpty()).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
}
|
if (!sortResult) return;
|
||||||
|
|
||||||
|
expect(await topics?.isEmpty()).toBeFalsy();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test("search topics", async ({ page }) => {
|
test.skip("search topics", async ({ page }) => {
|
||||||
const app = new AppModel(page);
|
const app = new AppModel(page);
|
||||||
await app.goto();
|
await app.goto();
|
||||||
const notebooks = await app.goToNotebooks();
|
const notebooks = await app.goToNotebooks();
|
||||||
@@ -155,7 +146,7 @@ test("search topics", async ({ page }) => {
|
|||||||
});
|
});
|
||||||
await notebook?.openNotebook();
|
await notebook?.openNotebook();
|
||||||
|
|
||||||
const search = await app.search("1");
|
const search = await app.search("1", "topics");
|
||||||
const topic = await search?.findItem({ title: "title1" });
|
const topic = await search?.findItem({ title: "title1" });
|
||||||
|
|
||||||
expect((await topic?.getTitle()) === "title1").toBeTruthy();
|
expect((await topic?.getTitle()) === "title1").toBeTruthy();
|
||||||
|
|||||||
1
apps/web/src/assets/note2.svg
Normal file
1
apps/web/src/assets/note2.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 5.6 KiB |
@@ -19,7 +19,6 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import ReactDOM from "react-dom";
|
import ReactDOM from "react-dom";
|
||||||
import { Dialogs } from "../components/dialogs";
|
import { Dialogs } from "../components/dialogs";
|
||||||
import { hardNavigate } from "../navigation";
|
|
||||||
import ThemeProvider from "../components/theme-provider";
|
import ThemeProvider from "../components/theme-provider";
|
||||||
import qclone from "qclone";
|
import qclone from "qclone";
|
||||||
import { store as notebookStore } from "../stores/notebook-store";
|
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 { store as noteStore } from "../stores/note-store";
|
||||||
import { db } from "./db";
|
import { db } from "./db";
|
||||||
import { showToast } from "../utils/toast";
|
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 * as Icon from "../components/icons";
|
||||||
import Config from "../utils/config";
|
import Config from "../utils/config";
|
||||||
import { formatDate } from "@notesnook/core/utils/date";
|
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 { AuthenticatorType } from "../components/dialogs/mfa/types";
|
||||||
import { Suspense } from "react";
|
import { Suspense } from "react";
|
||||||
import { Reminder } from "@notesnook/core/collections/reminders";
|
import { Reminder } from "@notesnook/core/collections/reminders";
|
||||||
|
import { ConfirmDialogProps } from "../components/dialogs/confirm";
|
||||||
|
|
||||||
type DialogTypes = typeof Dialogs;
|
type DialogTypes = typeof Dialogs;
|
||||||
type DialogIds = keyof DialogTypes;
|
type DialogIds = keyof DialogTypes;
|
||||||
export type Perform = (result: boolean) => void;
|
export type Perform<T = boolean> = (result: T) => void;
|
||||||
type RenderDialog<TId extends DialogIds, TReturnType> = (
|
type RenderDialog<TId extends DialogIds, TReturnType> = (
|
||||||
dialog: DialogTypes[TId],
|
dialog: DialogTypes[TId],
|
||||||
perform: (result: TReturnType) => void
|
perform: (result: TReturnType) => void
|
||||||
@@ -154,26 +154,15 @@ export function showBuyDialog(plan?: Period, couponCode?: string) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfirmDialogProps = {
|
export function confirm<TCheckId extends string>(
|
||||||
title?: string;
|
props: Omit<ConfirmDialogProps<TCheckId>, "onClose">
|
||||||
subtitle?: string;
|
) {
|
||||||
message?: string | JSX.Element;
|
return showDialog<"Confirm", false | Record<TCheckId, boolean>>(
|
||||||
yesText?: string;
|
"Confirm",
|
||||||
noText?: string;
|
(Dialog, perform) => (
|
||||||
yesAction?: () => void;
|
<Dialog {...props} onClose={(result) => perform(result)} />
|
||||||
width?: string;
|
)
|
||||||
};
|
);
|
||||||
export function confirm(props: ConfirmDialogProps) {
|
|
||||||
return showDialog("Confirm", (Dialog, perform) => (
|
|
||||||
<Dialog
|
|
||||||
{...props}
|
|
||||||
onNo={() => perform(false)}
|
|
||||||
onYes={() => {
|
|
||||||
if (props.yesAction) props.yesAction();
|
|
||||||
perform(true);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showPromptDialog(props: {
|
export function showPromptDialog(props: {
|
||||||
@@ -214,41 +203,26 @@ export function showToolbarConfigDialog() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function showError(title: string, message: string) {
|
export function showError(title: string, message: string) {
|
||||||
return confirm({ title, message, yesText: "Okay" });
|
return confirm({ title, message, positiveButtonText: "Okay" });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showMultiDeleteConfirmation(length: number) {
|
export function showMultiDeleteConfirmation(length: number) {
|
||||||
return confirm({
|
return confirm({
|
||||||
title: `Delete ${length} items?`,
|
title: `Delete ${length} items?`,
|
||||||
message: (
|
message:
|
||||||
<Text as="span">
|
"These items will be **kept in your Trash for 7 days** after which they will be permanently deleted.",
|
||||||
These items will be{" "}
|
positiveButtonText: "Yes",
|
||||||
<Text as="span" sx={{ color: "primary" }}>
|
negativeButtonText: "No"
|
||||||
kept in your Trash for 7 days
|
|
||||||
</Text>{" "}
|
|
||||||
after which they will be permanently removed.
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
yesText: `Delete selected`,
|
|
||||||
noText: "Cancel"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showMultiPermanentDeleteConfirmation(length: number) {
|
export function showMultiPermanentDeleteConfirmation(length: number) {
|
||||||
return confirm({
|
return confirm({
|
||||||
title: `Permanently delete ${length} items?`,
|
title: `Permanently delete ${length} items?`,
|
||||||
message: (
|
message:
|
||||||
<Text as="span">
|
"These items will be **permanently deleted**. This is IRREVERSIBLE.",
|
||||||
These items will be{" "}
|
positiveButtonText: "Yes",
|
||||||
<Text as="span" sx={{ color: "primary" }}>
|
negativeButtonText: "No"
|
||||||
permanently deleted
|
|
||||||
</Text>
|
|
||||||
{". "}
|
|
||||||
This action is IRREVERSIBLE.
|
|
||||||
</Text>
|
|
||||||
),
|
|
||||||
yesText: `Permanently delete selected`,
|
|
||||||
noText: "Cancel"
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,8 +231,8 @@ export function showLogoutConfirmation() {
|
|||||||
title: `Logout?`,
|
title: `Logout?`,
|
||||||
message:
|
message:
|
||||||
"Logging out will delete all local data and reset the app. Make sure you have synced your data before logging out.",
|
"Logging out will delete all local data and reset the app. Make sure you have synced your data before logging out.",
|
||||||
yesText: `Yes`,
|
positiveButtonText: "Yes",
|
||||||
noText: "No"
|
negativeButtonText: "No"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -267,8 +241,8 @@ export function showClearSessionsConfirmation() {
|
|||||||
title: `Logout from other devices?`,
|
title: `Logout from other devices?`,
|
||||||
message:
|
message:
|
||||||
"All other logged-in devices will be forced to logout stopping sync. Use with care lest you lose important notes.",
|
"All other logged-in devices will be forced to logout stopping sync. Use with care lest you lose important notes.",
|
||||||
yesText: `Yes`,
|
positiveButtonText: "Yes",
|
||||||
noText: "No"
|
negativeButtonText: "No"
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -276,9 +250,7 @@ export function showAccountLoggedOutNotice(reason?: string) {
|
|||||||
return confirm({
|
return confirm({
|
||||||
title: "You were logged out",
|
title: "You were logged out",
|
||||||
message: reason,
|
message: reason,
|
||||||
noText: "Okay",
|
negativeButtonText: "Okay"
|
||||||
yesText: `Relogin`,
|
|
||||||
yesAction: () => hardNavigate("/login")
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -287,24 +259,13 @@ export function showAppUpdatedNotice(
|
|||||||
) {
|
) {
|
||||||
return confirm({
|
return confirm({
|
||||||
title: `Welcome to v${version.formatted}`,
|
title: `Welcome to v${version.formatted}`,
|
||||||
message: (
|
message: `## Changelog:
|
||||||
<Flex
|
|
||||||
bg="bgSecondary"
|
\`\`\`
|
||||||
p={1}
|
${version.changelog || "No change log."}
|
||||||
sx={{ borderRadius: "default", flexDirection: "column" }}
|
\`\`\`
|
||||||
>
|
`,
|
||||||
<Text variant="title">Changelog:</Text>
|
positiveButtonText: `Continue`
|
||||||
<Text
|
|
||||||
as="pre"
|
|
||||||
variant="body"
|
|
||||||
mt={1}
|
|
||||||
sx={{ fontFamily: "monospace", overflow: "auto" }}
|
|
||||||
>
|
|
||||||
{version.changelog || "No change log."}
|
|
||||||
</Text>
|
|
||||||
</Flex>
|
|
||||||
),
|
|
||||||
yesText: `Yay!`
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,32 +667,29 @@ export function showOnboardingDialog(type: string) {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showInvalidSystemTimeDialog({
|
export async function showInvalidSystemTimeDialog({
|
||||||
serverTime,
|
serverTime,
|
||||||
localTime
|
localTime
|
||||||
}: {
|
}: {
|
||||||
serverTime: number;
|
serverTime: number;
|
||||||
localTime: number;
|
localTime: number;
|
||||||
}) {
|
}) {
|
||||||
return confirm({
|
const result = await confirm({
|
||||||
title: "Your system clock is out of sync",
|
title: "Your system clock is out of sync",
|
||||||
subtitle:
|
subtitle:
|
||||||
"Please correct your system date & time and reload the app to avoid syncing issues.",
|
"Please correct your system date & time and reload the app to avoid syncing issues.",
|
||||||
message: (
|
message: `Server time: ${formatDate(serverTime, {
|
||||||
<>
|
dateStyle: "medium",
|
||||||
Server time:{" "}
|
timeStyle: "medium"
|
||||||
{formatDate(serverTime, { dateStyle: "medium", timeStyle: "medium" })}
|
})}
|
||||||
<br />
|
Local time: ${formatDate(localTime, {
|
||||||
Local time:{" "}
|
dateStyle: "medium",
|
||||||
{formatDate(localTime, { dateStyle: "medium", timeStyle: "medium" })}
|
timeStyle: "medium"
|
||||||
<br />
|
})}
|
||||||
Please sync your system time with{" "}
|
Please sync your system time with [https://time.is](https://time.is).`,
|
||||||
<a href="https://time.is">https://time.is/</a>.
|
positiveButtonText: "Reload app"
|
||||||
</>
|
|
||||||
),
|
|
||||||
yesText: "Reload app",
|
|
||||||
yesAction: () => window.location.reload()
|
|
||||||
});
|
});
|
||||||
|
if (result) window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function showUpdateAvailableNotice({
|
export async function showUpdateAvailableNotice({
|
||||||
@@ -771,37 +729,18 @@ type UpdateDialogProps = {
|
|||||||
onClick: () => void;
|
onClick: () => void;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
function showUpdateDialog({
|
async function showUpdateDialog({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
changelog,
|
changelog,
|
||||||
action
|
action
|
||||||
}: UpdateDialogProps) {
|
}: UpdateDialogProps) {
|
||||||
return confirm({
|
const result = await confirm({
|
||||||
title,
|
title,
|
||||||
subtitle,
|
subtitle,
|
||||||
message: changelog && (
|
message: changelog,
|
||||||
<Flex sx={{ borderRadius: "default", flexDirection: "column" }}>
|
width: 500,
|
||||||
<Text
|
positiveButtonText: action.text
|
||||||
as="div"
|
|
||||||
variant="body"
|
|
||||||
sx={{ overflow: "auto", fontFamily: "body" }}
|
|
||||||
css={`
|
|
||||||
h2 {
|
|
||||||
font-size: 1.2em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
h3 {
|
|
||||||
font-size: 1em;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
dangerouslySetInnerHTML={{ __html: changelog }}
|
|
||||||
></Text>
|
|
||||||
</Flex>
|
|
||||||
),
|
|
||||||
width: "500px",
|
|
||||||
yesText: action.text,
|
|
||||||
yesAction: action.onClick
|
|
||||||
});
|
});
|
||||||
|
if (result && action.onClick) action.onClick();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { Box, Text } from "@theme-ui/components";
|
|
||||||
import Dialog from "./dialog";
|
|
||||||
|
|
||||||
function Confirm(props) {
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
isOpen={true}
|
|
||||||
title={props.title}
|
|
||||||
icon={props.icon}
|
|
||||||
width={props.width}
|
|
||||||
description={props.subtitle}
|
|
||||||
onClose={() => 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) }
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Box pb={!props.noText && !props.yesText ? 2 : 0}>
|
|
||||||
<Text as="span" variant="body">
|
|
||||||
{props.message}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Confirm;
|
|
||||||
120
apps/web/src/components/dialogs/confirm.tsx
Normal file
120
apps/web/src/components/dialogs/confirm.tsx
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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<TCheckId extends string> = {
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
onClose: Perform<false | Record<TCheckId, boolean>>;
|
||||||
|
width?: number;
|
||||||
|
positiveButtonText?: string;
|
||||||
|
negativeButtonText?: string;
|
||||||
|
message?: string;
|
||||||
|
checks?: Partial<Record<TCheckId, Check>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function ConfirmDialog<TCheckId extends string>(
|
||||||
|
props: ConfirmDialogProps<TCheckId>
|
||||||
|
) {
|
||||||
|
const {
|
||||||
|
onClose,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
width,
|
||||||
|
negativeButtonText,
|
||||||
|
positiveButtonText,
|
||||||
|
message,
|
||||||
|
checks
|
||||||
|
} = props;
|
||||||
|
const checkedItems = useRef<Record<TCheckId, boolean>>({} as any);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
isOpen={true}
|
||||||
|
title={title}
|
||||||
|
width={width}
|
||||||
|
description={subtitle}
|
||||||
|
onClose={() => onClose(false)}
|
||||||
|
positiveButton={
|
||||||
|
positiveButtonText
|
||||||
|
? {
|
||||||
|
text: positiveButtonText,
|
||||||
|
onClick: () => onClose(checkedItems.current),
|
||||||
|
autoFocus: !!positiveButtonText
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
negativeButton={
|
||||||
|
negativeButtonText
|
||||||
|
? {
|
||||||
|
text: negativeButtonText,
|
||||||
|
onClick: () => onClose(false)
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
pb: !negativeButtonText && !positiveButtonText ? 2 : 0,
|
||||||
|
p: { m: 0 }
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{message ? (
|
||||||
|
<Text
|
||||||
|
as="span"
|
||||||
|
variant="body"
|
||||||
|
dangerouslySetInnerHTML={{ __html: mdToHtml(message) }}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{checks
|
||||||
|
? Object.entries<Check | undefined>(checks).map(
|
||||||
|
([id, check]) =>
|
||||||
|
check && (
|
||||||
|
<Label
|
||||||
|
key={id}
|
||||||
|
id={id}
|
||||||
|
variant="text.body"
|
||||||
|
sx={{ alignItems: "center" }}
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
name={id}
|
||||||
|
defaultChecked={check.default}
|
||||||
|
sx={{ mr: "small", width: 18, height: 18 }}
|
||||||
|
onChange={(e) =>
|
||||||
|
(checkedItems.current[id as TCheckId] =
|
||||||
|
e.currentTarget.checked)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{check.text}{" "}
|
||||||
|
</Label>
|
||||||
|
)
|
||||||
|
)
|
||||||
|
: null}
|
||||||
|
</Box>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ConfirmDialog;
|
||||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Flex, Link, Text } from "@theme-ui/components";
|
import { Flex, Text } from "@theme-ui/components";
|
||||||
import { appVersion } from "../../utils/version";
|
import { appVersion } from "../../utils/version";
|
||||||
import Field from "../field";
|
import Field from "../field";
|
||||||
import Dialog from "./dialog";
|
import Dialog from "./dialog";
|
||||||
@@ -152,33 +152,14 @@ export default IssueDialog;
|
|||||||
function showIssueReportedDialog({ url }: { url: string }) {
|
function showIssueReportedDialog({ url }: { url: string }) {
|
||||||
return confirm({
|
return confirm({
|
||||||
title: "Thank you for reporting!",
|
title: "Thank you for reporting!",
|
||||||
yesAction: () => clipboard.writeText(url),
|
positiveButtonText: "Copy link",
|
||||||
yesText: "Copy link",
|
message: `You can track your bug report at [${url}](${url}).
|
||||||
message: (
|
|
||||||
<>
|
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.**
|
||||||
<p>
|
|
||||||
You can track your bug report at{" "}
|
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.`
|
||||||
<Link target="_blank" href={url} sx={{ lineBreak: "anywhere" }}>
|
}).then((result) => {
|
||||||
{url}
|
result && clipboard.writeText(url);
|
||||||
</Link>
|
|
||||||
.
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
Please note that we will respond to your bug report on the link above.{" "}
|
|
||||||
<b>
|
|
||||||
We recommended that you save the above link for later reference.
|
|
||||||
</b>
|
|
||||||
</p>
|
|
||||||
<p>
|
|
||||||
If your issue is critical (e.g. notes not syncing, crashes etc.),
|
|
||||||
please{" "}
|
|
||||||
<a href="https://discord.com/invite/zQBK97EE22">
|
|
||||||
join our Discord community
|
|
||||||
</a>{" "}
|
|
||||||
for one-to-one support.
|
|
||||||
</p>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -145,7 +145,9 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
|||||||
if (stringified) {
|
if (stringified) {
|
||||||
showToast(
|
showToast(
|
||||||
"success",
|
"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 (added.length > 1) parts.push(`and ${added.length - 1} others`);
|
||||||
|
|
||||||
if (removed.length >= 1) {
|
if (removed.length >= 1) {
|
||||||
parts.push("remove from");
|
parts.push("& remove from");
|
||||||
parts.push(removed[0]);
|
parts.push(removed[0]);
|
||||||
}
|
}
|
||||||
if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
|
if (removed.length > 1) parts.push(`and ${removed.length - 1} others`);
|
||||||
|
|
||||||
return parts.join(" ");
|
return parts.join(" ") + ".";
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolve(ref: NotebookReference) {
|
function resolve(ref: NotebookReference) {
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import * as Icon from "../icons";
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { Button, Flex, Text } from "@theme-ui/components";
|
import { Button, Flex, Text } from "@theme-ui/components";
|
||||||
import { db } from "../../common/db";
|
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 useNoteStore } from "../../stores/note-store";
|
||||||
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
||||||
import useMobile from "../../hooks/use-mobile";
|
import useMobile from "../../hooks/use-mobile";
|
||||||
@@ -36,84 +36,97 @@ const groupByToTitleMap = {
|
|||||||
month: "Month"
|
month: "Month"
|
||||||
};
|
};
|
||||||
|
|
||||||
const menuItems = [
|
const groupByMenu = {
|
||||||
{
|
key: "groupBy",
|
||||||
key: "sortDirection",
|
title: "Group by",
|
||||||
title: "Order by",
|
icon: Icon.GroupBy,
|
||||||
icon: ({ groupOptions }) =>
|
items: map([
|
||||||
groupOptions.sortDirection === "asc"
|
{ key: "none", title: "None" },
|
||||||
? groupOptions.sortBy === "title"
|
{ key: "default", title: "Default" },
|
||||||
? Icon.OrderAtoZ
|
{ key: "year", title: "Year" },
|
||||||
: Icon.OrderOldestNewest
|
{ key: "month", title: "Month" },
|
||||||
: groupOptions.sortBy === "title"
|
{ key: "week", title: "Week" },
|
||||||
? Icon.OrderZtoA
|
{ key: "abc", title: "A - Z" }
|
||||||
: Icon.OrderNewestOldest,
|
])
|
||||||
items: map([
|
};
|
||||||
{
|
|
||||||
key: "asc",
|
const orderByMenu = {
|
||||||
title: ({ groupOptions }) =>
|
key: "sortDirection",
|
||||||
groupOptions.sortBy === "title" ? "A - Z" : "Oldest - newest"
|
title: "Order by",
|
||||||
},
|
icon: ({ groupOptions }) =>
|
||||||
{
|
groupOptions.sortDirection === "asc"
|
||||||
key: "desc",
|
? groupOptions.sortBy === "title"
|
||||||
title: ({ groupOptions }) =>
|
? Icon.OrderAtoZ
|
||||||
groupOptions.sortBy === "title" ? "Z - A" : "Newest - oldest"
|
: 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",
|
export function showSortMenu(type, refresh) {
|
||||||
icon: Icon.SortBy,
|
const groupOptions = db.settings.getGroupOptions(type);
|
||||||
items: map([
|
Menu.openMenu([orderByMenu, sortByMenu], {
|
||||||
{
|
title: "Sort",
|
||||||
key: "dateCreated",
|
groupOptions,
|
||||||
title: "Date created",
|
refresh,
|
||||||
hidden: ({ type }) => type === "trash"
|
type,
|
||||||
},
|
isUngrouped: true
|
||||||
{
|
});
|
||||||
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" }
|
|
||||||
])
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
function changeGroupOptions({ groupOptions, type, refresh, parent }, item) {
|
function changeGroupOptions({ groupOptions, type, refresh, parent }, item) {
|
||||||
if (!parent) return false;
|
if (!parent) return false;
|
||||||
@@ -132,19 +145,10 @@ function isChecked({ groupOptions, parent }, item) {
|
|||||||
return groupOptions[parent.key] === item.key;
|
return groupOptions[parent.key] === item.key;
|
||||||
}
|
}
|
||||||
|
|
||||||
function isDisabled({ groupOptions, parent }, item) {
|
|
||||||
return (
|
|
||||||
parent?.key === "sortBy" &&
|
|
||||||
item.key === "title" &&
|
|
||||||
groupOptions.groupBy === "abc"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function map(items) {
|
function map(items) {
|
||||||
return items.map((item) => {
|
return items.map((item) => {
|
||||||
item.checked = isChecked;
|
item.checked = isChecked;
|
||||||
item.onClick = changeGroupOptions;
|
item.onClick = changeGroupOptions;
|
||||||
item.disabled = isDisabled;
|
|
||||||
return item;
|
return item;
|
||||||
}, []);
|
}, []);
|
||||||
}
|
}
|
||||||
@@ -248,7 +252,7 @@ function GroupHeader(props) {
|
|||||||
<Flex mr={1}>
|
<Flex mr={1}>
|
||||||
{type && (
|
{type && (
|
||||||
<IconButton
|
<IconButton
|
||||||
testId="sort-icon-button"
|
testId={`${type}-sort-button`}
|
||||||
icon={
|
icon={
|
||||||
groupOptions.sortDirection === "asc"
|
groupOptions.sortDirection === "asc"
|
||||||
? Icon.SortAsc
|
? Icon.SortAsc
|
||||||
@@ -259,7 +263,7 @@ function GroupHeader(props) {
|
|||||||
const groupOptions = db.settings.getGroupOptions(type);
|
const groupOptions = db.settings.getGroupOptions(type);
|
||||||
setGroupOptions(groupOptions);
|
setGroupOptions(groupOptions);
|
||||||
|
|
||||||
openMenu(menuItems, {
|
openMenu([orderByMenu, sortByMenu, groupByMenu], {
|
||||||
title: "Group & sort",
|
title: "Group & sort",
|
||||||
groupOptions,
|
groupOptions,
|
||||||
refresh,
|
refresh,
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
transition={{ duration: 0.2, delay: 0.1, ease: "easeInOut" }}
|
transition={{ duration: 0.2, delay: 0.1, ease: "easeInOut" }}
|
||||||
ref={listContainerRef}
|
ref={listContainerRef}
|
||||||
variant="columnFill"
|
variant="columnFill"
|
||||||
data-test-id="note-list"
|
data-test-id={`${type}-list`}
|
||||||
>
|
>
|
||||||
<Virtuoso
|
<Virtuoso
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
@@ -187,6 +187,7 @@ function ListContainer(props: ListContainerProps) {
|
|||||||
|
|
||||||
switch (item.type) {
|
switch (item.type) {
|
||||||
case "header":
|
case "header":
|
||||||
|
if (!groupType) return null;
|
||||||
return (
|
return (
|
||||||
<GroupHeader
|
<GroupHeader
|
||||||
type={groupType}
|
type={groupType}
|
||||||
|
|||||||
@@ -431,8 +431,8 @@ const menuItems = [
|
|||||||
await confirm({
|
await confirm({
|
||||||
title: "Open duplicated note?",
|
title: "Open duplicated note?",
|
||||||
message: "Do you want to open the duplicated note?",
|
message: "Do you want to open the duplicated note?",
|
||||||
noText: "No",
|
negativeButtonText: "No",
|
||||||
yesText: "Yes"
|
positiveButtonText: "Yes"
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
hashNavigate(`/notes/${id}/edit`, { replace: true });
|
hashNavigate(`/notes/${id}/edit`, { replace: true });
|
||||||
@@ -455,8 +455,8 @@ const menuItems = [
|
|||||||
title: "Prevent this item from syncing?",
|
title: "Prevent this item from syncing?",
|
||||||
message:
|
message:
|
||||||
"Turning sync off for this item will automatically delete it from all other devices & any future changes to this item won't get synced. Are you sure you want to continue?",
|
"Turning sync off for this item will automatically delete it from all other devices & any future changes to this item won't get synced. Are you sure you want to continue?",
|
||||||
yesText: "Yes",
|
positiveButtonText: "Yes",
|
||||||
noText: "No"
|
negativeButtonText: "No"
|
||||||
}))
|
}))
|
||||||
)
|
)
|
||||||
await store.get().localOnly(note.id);
|
await store.get().localOnly(note.id);
|
||||||
|
|||||||
@@ -155,25 +155,31 @@ const menuItems = [
|
|||||||
iconColor: "error",
|
iconColor: "error",
|
||||||
icon: Icon.Trash,
|
icon: Icon.Trash,
|
||||||
onClick: async ({ items }) => {
|
onClick: async ({ items }) => {
|
||||||
const phrase = items.length > 1 ? "this notebook" : "these notebooks";
|
const result = await confirm({
|
||||||
const shouldDeleteNotes = await confirm({
|
title: `Delete ${pluralize(items.length, "notebook", "notebooks")}?`,
|
||||||
title: `Delete notes in ${phrase}?`,
|
positiveButtonText: `Yes`,
|
||||||
message: `These notes will be moved to trash and permanently deleted after 7 days.`,
|
negativeButtonText: "No",
|
||||||
yesText: `Yes`,
|
checks: {
|
||||||
noText: "No"
|
deleteContainingNotes: {
|
||||||
});
|
text: `Delete all containing notes`
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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
|
multiSelect: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import ListItem from "../list-item";
|
import ListItem from "../list-item";
|
||||||
import { Flex, Text } from "@theme-ui/components";
|
import { Flex } from "@theme-ui/components";
|
||||||
import * as Icon from "../icons";
|
import * as Icon from "../icons";
|
||||||
import IconTag from "../icon-tag";
|
import IconTag from "../icon-tag";
|
||||||
import {
|
import {
|
||||||
@@ -164,15 +164,11 @@ const menuItems: MenuItem[] = [
|
|||||||
onClick: async ({ items }) => {
|
onClick: async ({ items }) => {
|
||||||
confirm({
|
confirm({
|
||||||
title: `Delete ${pluralize(items.length, "reminder", "reminders")}`,
|
title: `Delete ${pluralize(items.length, "reminder", "reminders")}`,
|
||||||
message: (
|
message: `Are you sure you want to proceed? **This action is IRREVERSIBLE**.`,
|
||||||
<Text>
|
positiveButtonText: "Yes",
|
||||||
Are you sure you want to proceed?
|
negativeButtonText: "No"
|
||||||
<Text sx={{ color: "error" }}> This action is IRREVERSIBLE.</Text>
|
}).then((result) => {
|
||||||
</Text>
|
result && Multiselect.moveRemindersToTrash(items);
|
||||||
),
|
|
||||||
yesText: "Yes",
|
|
||||||
noText: "No",
|
|
||||||
yesAction: () => Multiselect.moveRemindersToTrash(items)
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
multiSelect: true
|
multiSelect: true
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import * as Icon from "../icons";
|
|||||||
import { Multiselect } from "../../common/multi-select";
|
import { Multiselect } from "../../common/multi-select";
|
||||||
import { confirm } from "../../common/dialog-controller";
|
import { confirm } from "../../common/dialog-controller";
|
||||||
import { useStore as useNotesStore } from "../../stores/note-store";
|
import { useStore as useNotesStore } from "../../stores/note-store";
|
||||||
|
import { pluralize } from "../../utils/string";
|
||||||
|
|
||||||
function Topic({ item, index, onClick }) {
|
function Topic({ item, index, onClick }) {
|
||||||
const { id, notebookId } = item;
|
const { id, notebookId } = item;
|
||||||
@@ -36,7 +37,7 @@ function Topic({ item, index, onClick }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const totalNotes = useMemo(() => {
|
const totalNotes = useMemo(() => {
|
||||||
return db.notebooks.notebook(notebookId)?.topics.topic(id).totalNotes;
|
return db.notebooks.notebook(notebookId)?.topics.topic(id)?.totalNotes || 0;
|
||||||
}, [id, notebookId]);
|
}, [id, notebookId]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -94,23 +95,30 @@ const menuItems = [
|
|||||||
color: "error",
|
color: "error",
|
||||||
iconColor: "error",
|
iconColor: "error",
|
||||||
onClick: async ({ items, notebookId }) => {
|
onClick: async ({ items, notebookId }) => {
|
||||||
const phrase = items.length > 1 ? "this topic" : "these topics";
|
const result = await confirm({
|
||||||
const shouldDeleteNotes = await confirm({
|
title: `Delete ${pluralize(items.length, "topic", "topics")}?`,
|
||||||
title: `Delete notes in ${phrase}?`,
|
positiveButtonText: `Yes`,
|
||||||
message: `These notes will be moved to trash and permanently deleted after 7 days.`,
|
negativeButtonText: "No",
|
||||||
yesText: `Yes`,
|
checks: {
|
||||||
noText: "No"
|
deleteContainingNotes: {
|
||||||
|
text: `Delete all containing notes`
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (shouldDeleteNotes) {
|
if (result) {
|
||||||
const notes = [];
|
if (result.deleteContainingNotes) {
|
||||||
for (const item of items) {
|
const notes = [];
|
||||||
const topic = db.notebooks.notebook(notebookId).topics.topic(item.id);
|
for (const item of items) {
|
||||||
notes.push(...topic.all);
|
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
|
multiSelect: true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
export function useMenu() {
|
||||||
const [items, data] = useMenuStore((store) => [store.items, store.data]);
|
const [items, data] = useMenuStore((store) => [store.items, store.data]);
|
||||||
return { items, data };
|
return { items, data };
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ const routes = {
|
|||||||
value: { id: notebookId, topic: topicId }
|
value: { id: notebookId, topic: topicId }
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
key: "topic",
|
key: "notebook",
|
||||||
type: "notebook",
|
type: "notebook",
|
||||||
title: topic.title,
|
title: topic.title,
|
||||||
component: <Topics />,
|
component: <Topics />,
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ import Config from "../utils/config";
|
|||||||
|
|
||||||
class NotebookStore extends BaseStore {
|
class NotebookStore extends BaseStore {
|
||||||
notebooks = [];
|
notebooks = [];
|
||||||
selectedNotebookId = 0;
|
selectedNotebook = undefined;
|
||||||
|
selectedNotebookTopics = [];
|
||||||
viewMode = Config.get("notebooks:viewMode", "detailed");
|
viewMode = Config.get("notebooks:viewMode", "detailed");
|
||||||
|
|
||||||
setViewMode = (viewMode) => {
|
setViewMode = (viewMode) => {
|
||||||
@@ -42,7 +43,7 @@ class NotebookStore extends BaseStore {
|
|||||||
db.settings.getGroupOptions("notebooks")
|
db.settings.getGroupOptions("notebooks")
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
this.setSelectedNotebook(this.get().selectedNotebookId);
|
this.setSelectedNotebook(this.get().selectedNotebook?.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
delete = async (...ids) => {
|
delete = async (...ids) => {
|
||||||
@@ -59,8 +60,16 @@ class NotebookStore extends BaseStore {
|
|||||||
};
|
};
|
||||||
|
|
||||||
setSelectedNotebook = (id) => {
|
setSelectedNotebook = (id) => {
|
||||||
|
if (!id) return;
|
||||||
|
const notebook = db.notebooks?.notebook(id)?.data;
|
||||||
|
if (!notebook) return;
|
||||||
|
|
||||||
this.set((state) => {
|
this.set((state) => {
|
||||||
state.selectedNotebookId = id;
|
state.selectedNotebook = notebook;
|
||||||
|
state.selectedNotebookTopics = groupArray(
|
||||||
|
notebook.topics,
|
||||||
|
db.settings.getGroupOptions("topics")
|
||||||
|
);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,38 +17,6 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
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 `<span className="emoji ${token}" />`;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderer = new marked.Renderer();
|
|
||||||
renderer.link = function (href, title, text) {
|
|
||||||
return `<a target="_blank" rel="noopener noreferrer" href="${href}" ${
|
|
||||||
title ? `title=${title}` : ""
|
|
||||||
}>${text}</a>`;
|
|
||||||
};
|
|
||||||
marked.use({ extensions: [emoji] });
|
|
||||||
|
|
||||||
export async function getChangelog(tag: string) {
|
export async function getChangelog(tag: string) {
|
||||||
try {
|
try {
|
||||||
if (!tag) return "No changelog found.";
|
if (!tag) return "No changelog found.";
|
||||||
@@ -63,7 +31,7 @@ export async function getChangelog(tag: string) {
|
|||||||
if (!release || !release.body) return "No changelog found.";
|
if (!release || !release.body) return "No changelog found.";
|
||||||
|
|
||||||
const { body } = release;
|
const { body } = release;
|
||||||
return await marked.parse(body, { async: true, renderer, gfm: true });
|
return body;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return "No changelog found.";
|
return "No changelog found.";
|
||||||
|
|||||||
54
apps/web/src/utils/md.ts
Normal file
54
apps/web/src/utils/md.ts
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
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 `<span className="emoji ${token}" />`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderer = new marked.Renderer();
|
||||||
|
renderer.link = function (href, title, text) {
|
||||||
|
return `<a target="_blank" rel="noopener noreferrer" href="${href}" ${
|
||||||
|
title ? `title=${title}` : ""
|
||||||
|
}>${text}</a>`;
|
||||||
|
};
|
||||||
|
marked.use({ extensions: [emoji] });
|
||||||
|
|
||||||
|
export function mdToHtml(markdown: string) {
|
||||||
|
return marked.parse(markdown, { async: false, renderer, gfm: true });
|
||||||
|
}
|
||||||
@@ -755,6 +755,7 @@ function MFASelector(props: BaseAuthComponentProps<"mfa:select">) {
|
|||||||
(method, index) =>
|
(method, index) =>
|
||||||
isValidMethod(method.type) && (
|
isValidMethod(method.type) && (
|
||||||
<Button
|
<Button
|
||||||
|
key={method.type}
|
||||||
type="submit"
|
type="submit"
|
||||||
variant={"secondary"}
|
variant={"secondary"}
|
||||||
mt={2}
|
mt={2}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ You should have received a copy of the GNU General Public License
|
|||||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import ListContainer from "../components/list-container";
|
import ListContainer from "../components/list-container";
|
||||||
import { useStore as useNbStore } from "../stores/notebook-store";
|
import { useStore as useNbStore } from "../stores/notebook-store";
|
||||||
import { useStore as useAppStore } from "../stores/app-store";
|
import { useStore as useAppStore } from "../stores/app-store";
|
||||||
@@ -28,37 +28,37 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Edit,
|
Edit,
|
||||||
RemoveShortcutLink,
|
RemoveShortcutLink,
|
||||||
ShortcutLink
|
ShortcutLink,
|
||||||
|
SortAsc
|
||||||
} from "../components/icons";
|
} from "../components/icons";
|
||||||
import { getTotalNotes } from "../common";
|
import { getTotalNotes } from "../common";
|
||||||
import { formatDate } from "@notesnook/core/utils/date";
|
import { formatDate } from "@notesnook/core/utils/date";
|
||||||
import { db } from "../common/db";
|
|
||||||
import { pluralize } from "../utils/string";
|
import { pluralize } from "../utils/string";
|
||||||
import { Allotment } from "allotment";
|
import { Allotment } from "allotment";
|
||||||
import { Plus } from "../components/icons";
|
import { Plus } from "../components/icons";
|
||||||
import { useStore as useNotesStore } from "../stores/note-store";
|
import { useStore as useNotesStore } from "../stores/note-store";
|
||||||
import Placeholder from "../components/placeholders";
|
import Placeholder from "../components/placeholders";
|
||||||
|
import { showSortMenu } from "../components/group-header";
|
||||||
|
|
||||||
function Notebook() {
|
function Notebook() {
|
||||||
const [isCollapsed, setIsCollapsed] = useState(false);
|
const [isCollapsed, setIsCollapsed] = useState(false);
|
||||||
|
|
||||||
const selectedNotebookId = useNbStore((store) => store.selectedNotebookId);
|
const selectedNotebook = useNbStore((store) => store.selectedNotebook);
|
||||||
const refresh = useNbStore((store) => store.setSelectedNotebook);
|
const refresh = useNbStore((store) => store.setSelectedNotebook);
|
||||||
const notebooks = useNbStore((store) => store.notebooks);
|
|
||||||
|
|
||||||
const context = useNotesStore((store) => store.context);
|
const context = useNotesStore((store) => store.context);
|
||||||
const refreshContext = useNotesStore((store) => store.refreshContext);
|
const refreshContext = useNotesStore((store) => store.refreshContext);
|
||||||
const isCompact = useNotesStore((store) => store.viewMode === "compact");
|
const isCompact = useNotesStore((store) => store.viewMode === "compact");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (context && context.value && selectedNotebookId !== context.value.id)
|
if (
|
||||||
|
context &&
|
||||||
|
context.value &&
|
||||||
|
selectedNotebook &&
|
||||||
|
selectedNotebook.id !== context.value.id
|
||||||
|
)
|
||||||
refresh(context.value.id);
|
refresh(context.value.id);
|
||||||
}, [selectedNotebookId, context, refresh]);
|
}, [selectedNotebook, context, refresh]);
|
||||||
|
|
||||||
const selectedNotebook = useMemo(
|
|
||||||
() => db.notebooks?.notebook(selectedNotebookId)?.data,
|
|
||||||
[selectedNotebookId, notebooks]
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!context) return null;
|
if (!context) return null;
|
||||||
return (
|
return (
|
||||||
@@ -69,7 +69,7 @@ function Notebook() {
|
|||||||
{ title: "Notebooks", onClick: () => navigate(`/notebooks/`) },
|
{ title: "Notebooks", onClick: () => navigate(`/notebooks/`) },
|
||||||
{
|
{
|
||||||
title: selectedNotebook.title,
|
title: selectedNotebook.title,
|
||||||
onClick: () => navigate(`/notebooks/${selectedNotebookId}`)
|
onClick: () => navigate(`/notebooks/${selectedNotebook.id}`)
|
||||||
}
|
}
|
||||||
].map((crumb, index, array) => (
|
].map((crumb, index, array) => (
|
||||||
<>
|
<>
|
||||||
@@ -139,8 +139,11 @@ export default Notebook;
|
|||||||
|
|
||||||
function Topics({ selectedNotebook, isCollapsed, setIsCollapsed }) {
|
function Topics({ selectedNotebook, isCollapsed, setIsCollapsed }) {
|
||||||
const refresh = useNbStore((store) => store.setSelectedNotebook);
|
const refresh = useNbStore((store) => store.setSelectedNotebook);
|
||||||
|
const topics = useNbStore((store) => store.selectedNotebookTopics);
|
||||||
|
|
||||||
|
if (!selectedNotebook) return null;
|
||||||
return (
|
return (
|
||||||
<Flex variant="columnFill" sx={{ height: "100%" }}>
|
<Flex id="topics" variant="columnFill" sx={{ height: "100%" }}>
|
||||||
<Flex
|
<Flex
|
||||||
sx={{
|
sx={{
|
||||||
m: 1,
|
m: 1,
|
||||||
@@ -161,26 +164,42 @@ function Topics({ selectedNotebook, isCollapsed, setIsCollapsed }) {
|
|||||||
TOPICS
|
TOPICS
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Button
|
<Flex sx={{ alignItems: "center" }}>
|
||||||
variant="tool"
|
<Button
|
||||||
sx={{
|
variant="tool"
|
||||||
p: "1px",
|
data-test-id="topics-sort-button"
|
||||||
bg: "transparent",
|
sx={{
|
||||||
visibility: isCollapsed ? "collapse" : "visible"
|
p: "3.5px",
|
||||||
}}
|
bg: "transparent",
|
||||||
onClick={(e) => {
|
visibility: isCollapsed ? "collapse" : "visible"
|
||||||
hashNavigate(`/topics/create`);
|
}}
|
||||||
}}
|
onClick={(e) => {
|
||||||
>
|
e.stopPropagation();
|
||||||
<Plus size={20} />
|
showSortMenu("topics", () => refresh(selectedNotebook.id));
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
<SortAsc size={15} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="tool"
|
||||||
|
sx={{
|
||||||
|
p: "1px",
|
||||||
|
bg: "transparent",
|
||||||
|
visibility: isCollapsed ? "collapse" : "visible"
|
||||||
|
}}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
hashNavigate(`/topics/create`);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Plus size={20} />
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<ListContainer
|
<ListContainer
|
||||||
type="topics"
|
type="topics"
|
||||||
groupType="topics"
|
items={topics}
|
||||||
refresh={() => refresh(selectedNotebook.id)}
|
|
||||||
items={selectedNotebook.topics}
|
|
||||||
context={{
|
context={{
|
||||||
notebookId: selectedNotebook.id
|
notebookId: selectedNotebook.id
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -51,20 +51,9 @@ function Trash() {
|
|||||||
confirm({
|
confirm({
|
||||||
title: "Clear Trash",
|
title: "Clear Trash",
|
||||||
subtitle: "Are you sure you want to clear all the trash?",
|
subtitle: "Are you sure you want to clear all the trash?",
|
||||||
yesText: "Clear trash",
|
positiveButtonText: "Clear trash",
|
||||||
noText: "Cancel",
|
negativeButtonText: "Cancel",
|
||||||
message: (
|
message: `Are you sure you want to proceed? **This action is IRREVERSIBLE**.`
|
||||||
<>
|
|
||||||
This action is{" "}
|
|
||||||
<Text as="span" sx={{ color: "error" }}>
|
|
||||||
IRREVERSIBLE
|
|
||||||
</Text>
|
|
||||||
. You will{" "}
|
|
||||||
<Text as="span" sx={{ color: "primary" }}>
|
|
||||||
not be able to recover any of these items.
|
|
||||||
</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}).then(async (res) => {
|
}).then(async (res) => {
|
||||||
if (res) {
|
if (res) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user