web: fix notes tests

This commit is contained in:
Abdullah Atta
2023-11-24 15:04:10 +05:00
committed by Abdullah Atta
parent a51f84c1f4
commit fc270459b0
33 changed files with 316 additions and 224 deletions

View File

@@ -61,7 +61,7 @@ export class AppModel {
async goToNotes() {
await this.navigateTo("Notes");
return new NotesViewModel(this.page, "home");
return new NotesViewModel(this.page, "home", "home");
}
async goToNotebooks() {
@@ -71,7 +71,7 @@ export class AppModel {
async goToFavorites() {
await this.navigateTo("Favorites");
return new NotesViewModel(this.page, "notes");
return new NotesViewModel(this.page, "notes", "favorites");
}
async goToReminders() {
@@ -86,7 +86,7 @@ export class AppModel {
async goToColor(color: string) {
await this.navigateTo(color);
return new NotesViewModel(this.page, "notes");
return new NotesViewModel(this.page, "notes", "notes");
}
async goToTrash() {

View File

@@ -29,12 +29,15 @@ export class BaseViewModel {
private readonly listPlaceholder: Locator;
private readonly sortByButton: Locator;
constructor(page: Page, pageId: string, listType: string) {
constructor(page: Page, pageId: string, readonly listType: string) {
this.page = page;
this.list = page.locator(`#${pageId} >> ${getTestId(`${listType}-list`)}`);
this.listPlaceholder = page.locator(
`#${pageId} >> ${getTestId("list-placeholder")}`
);
this.list = page
.locator(`#${pageId}`)
.locator(getTestId(`${listType}-list`));
this.listPlaceholder = page
.locator(`#${pageId}`)
.locator(getTestId("list-placeholder"));
this.sortByButton = this.page.locator(
// TODO:
@@ -43,9 +46,9 @@ export class BaseViewModel {
}
async findGroup(groupName: string) {
const locator = this.list.locator(
`${getTestId(`virtuoso-item-list`)} >> ${getTestId("group-header")}`
);
const locator = this.list
.locator(getTestId(`virtualized-list`))
.locator(getTestId("group-header"));
for await (const item of iterateList(locator)) {
if ((await item.locator(getTestId("title")).textContent()) === groupName)
@@ -56,13 +59,10 @@ export class BaseViewModel {
protected async *iterateItems() {
await this.waitForList();
const locator = this.list.locator(
`${getTestId(`virtuoso-item-list`)} >> ${getTestId("list-item")}`
);
for await (const _item of iterateList(locator)) {
for await (const _item of iterateList(this.items)) {
const id = await _item.getAttribute("id");
if (!id) return;
if (!id) continue;
yield this.list.locator(`#${id}`);
}
@@ -82,11 +82,8 @@ export class BaseViewModel {
}
async focus() {
const items = this.list.locator(
`${getTestId(`virtuoso-item-list`)} >> ${getTestId("list-item")}`
);
await items.nth(0).click();
await items.nth(0).click();
await this.items.nth(0).click();
await this.items.nth(0).click();
}
// async selectAll() {
@@ -98,7 +95,7 @@ export class BaseViewModel {
// }
async press(key: string) {
const itemList = this.list.locator(getTestId(`virtuoso-item-list`));
const itemList = this.list.locator(getTestId(`virtualized-list`));
await itemList.press(key);
await this.page.waitForTimeout(300);
}
@@ -135,11 +132,11 @@ export class BaseViewModel {
return true;
}
get items() {
return this.list.locator(getTestId("list-item"));
}
async isEmpty() {
const items = this.list.locator(
`${getTestId(`virtuoso-item-list`)} >> ${getTestId("list-item")}`
);
const totalItems = await items.count();
return totalItems <= 0;
return (await this.items.count()) <= 0;
}
}

View File

@@ -44,7 +44,7 @@ export class EditorModel {
this.title = page.locator(getTestId("editor-title"));
this.content = page.locator(".ProseMirror");
this.tagInput = page.locator(getTestId("editor-tag-input"));
this.tags = page.locator(`${getTestId("tags")} > ${getTestId("tag")}`);
this.tags = page.locator(`${getTestId("tags")} >> ${getTestId("tag")}`);
this.focusModeButton = page.locator(getTestId("Focus mode"));
this.normalModeButton = page.locator(getTestId("Normal mode"));
this.darkModeButton = page.locator(getTestId("Dark mode"));

View File

@@ -23,6 +23,7 @@ import { ContextMenuModel } from "./context-menu.model";
import { NotesViewModel } from "./notes-view.model";
import { Item } from "./types";
import { confirmDialog, fillItemDialog } from "./utils";
import { getTestId } from "../utils";
export class ItemModel extends BaseItemModel {
private readonly contextMenu: ContextMenuModel;
@@ -35,7 +36,8 @@ export class ItemModel extends BaseItemModel {
await this.locator.click();
return new NotesViewModel(
this.page,
this.id === "topic" ? "notebook" : "notes"
this.id === "topic" ? "notebook" : "notes",
"notes"
);
}
@@ -53,7 +55,7 @@ export class ItemModel extends BaseItemModel {
if (deleteContainedNotes)
await this.page.locator("#deleteContainingNotes").check({ force: true });
await confirmDialog(this.page);
await confirmDialog(this.page.locator(getTestId("confirm-dialog")));
await this.waitFor("detached");
}

View File

@@ -22,7 +22,12 @@ import { downloadAndReadFile, getTestId } from "../utils";
import { ContextMenuModel } from "./context-menu.model";
import { ToggleModel } from "./toggle.model";
import { Notebook } from "./types";
import { fillPasswordDialog, iterateList } from "./utils";
import {
confirmDialog,
fillNotebookDialog,
fillPasswordDialog,
iterateList
} from "./utils";
abstract class BaseProperties {
protected readonly page: Page;
@@ -248,51 +253,59 @@ export class NoteContextMenuModel extends BaseProperties {
}
async addToNotebook(notebook: Notebook) {
async function addSubNotebooks(
page: Page,
dialog: Locator,
item: Locator,
notebook: Notebook
) {
if (notebook.subNotebooks) {
const addSubNotebookButton = item.locator(
getTestId("add-sub-notebook")
);
for (const subNotebook of notebook.subNotebooks) {
await addSubNotebookButton.click();
await fillNotebookDialog(page, subNotebook);
const subNotebookItem = dialog.locator(getTestId("notebook"), {
hasText: subNotebook.title
});
await subNotebookItem.waitFor();
await page.keyboard.down("Control");
await subNotebookItem.click();
await page.keyboard.up("Control");
await addSubNotebooks(page, dialog, subNotebookItem, subNotebook);
}
}
}
await this.open();
await this.menu.clickOnItem("notebooks");
await this.menu.clickOnItem("link-notebooks");
const filterInput = this.page.locator(getTestId("filter-input"));
await filterInput.type(notebook.title);
await filterInput.press("Enter");
const dialog = this.page.locator(getTestId("move-note-dialog"));
await this.page.waitForSelector(getTestId("notebook"), {
state: "visible",
strict: false
await dialog.locator(getTestId("add-new-notebook")).click();
await fillNotebookDialog(this.page, notebook);
const notebookItem = dialog.locator(getTestId("notebook"), {
hasText: notebook.title
});
const notebookItems = this.page.locator(getTestId("notebook"));
for await (const item of iterateList(notebookItems)) {
await item.locator(getTestId("notebook-tools")).click();
const title = item.locator(getTestId("notebook-title"));
const createTopicButton = item.locator(getTestId("create-topic"));
const notebookTitle = await title.textContent();
await notebookItem.waitFor({ state: "visible" });
if (notebookTitle?.includes(notebook.title)) {
for (const topic of notebook.topics) {
await createTopicButton.click();
const newItemInput = item.locator(getTestId("new-topic-input"));
await this.page.keyboard.down("Control");
await notebookItem.click();
await this.page.keyboard.up("Control");
await newItemInput.waitFor({ state: "visible" });
await newItemInput.fill(topic);
await newItemInput.press("Enter");
await addSubNotebooks(this.page, dialog, notebookItem, notebook);
await item.locator(getTestId("topic"), { hasText: topic }).waitFor();
}
const topicItems = item.locator(getTestId("topic"));
for await (const topicItem of iterateList(topicItems)) {
await this.page.keyboard.down("Control");
await topicItem.click();
await this.page.keyboard.up("Control");
}
}
}
const dialogConfirm = this.page.locator(getTestId("dialog-yes"));
await dialogConfirm.click();
await dialogConfirm.waitFor({ state: "detached" });
await confirmDialog(dialog);
}
async open() {

View File

@@ -25,6 +25,7 @@ import { ItemsViewModel } from "./items-view.model";
import { Notebook } from "./types";
import { confirmDialog, fillNotebookDialog } from "./utils";
import { NotesViewModel } from "./notes-view.model";
import { getTestId } from "../utils";
export class NotebookItemModel extends BaseItemModel {
private readonly contextMenu: ContextMenuModel;
@@ -37,7 +38,7 @@ export class NotebookItemModel extends BaseItemModel {
await this.locator.click();
return {
topics: new ItemsViewModel(this.page, "topics"),
notes: new NotesViewModel(this.page, "notebook")
notes: new NotesViewModel(this.page, "notebook", "notes")
};
}
@@ -45,7 +46,7 @@ export class NotebookItemModel extends BaseItemModel {
await this.contextMenu.open(this.locator);
await this.contextMenu.clickOnItem("edit");
await fillNotebookDialog(this.page, notebook, true);
await fillNotebookDialog(this.page, notebook);
}
async moveToTrash(deleteContainedNotes = false) {
@@ -55,7 +56,7 @@ export class NotebookItemModel extends BaseItemModel {
if (deleteContainedNotes)
await this.page.locator("#deleteContainingNotes").check({ force: true });
await confirmDialog(this.page);
await confirmDialog(this.page.locator(getTestId("confirm-dialog")));
await this.waitFor("detached");
}

View File

@@ -32,8 +32,12 @@ export class NotesViewModel extends BaseViewModel {
private readonly createButton: Locator;
readonly editor: EditorModel;
constructor(page: Page, pageId: "home" | "notes" | "notebook") {
super(page, pageId, pageId === "home" ? "home" : "notes");
constructor(
page: Page,
pageId: "home" | "notes" | "favorites" | "notebook",
listType: string
) {
super(page, pageId, listType);
this.createButton = page.locator(
// TODO:
getTestId(`notes-action-button`)

View File

@@ -47,7 +47,7 @@ export class ReminderItemModel extends BaseItemModel {
await this.contextMenu.open(this.locator);
await this.contextMenu.clickOnItem("delete");
await confirmDialog(this.page);
await confirmDialog(this.page.locator(getTestId("confirm-dialog")));
await this.waitFor("detached");
}

View File

@@ -52,7 +52,7 @@ export class SettingsViewModel {
.locator("button");
await logoutButton.click();
await confirmDialog(this.page);
await confirmDialog(this.page.locator(getTestId("confirm-dialog")));
await this.page
.locator(getTestId("not-logged-in"))
@@ -75,7 +75,9 @@ export class SettingsViewModel {
const key = await this.page
.locator(getTestId("recovery-key"))
.textContent();
await confirmDialog(this.page);
const dialog = this.page.locator(getTestId("recovery-key-dialog"));
await confirmDialog(dialog);
return key;
}

View File

@@ -19,8 +19,8 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
export type Notebook = {
title: string;
topics: string[];
description?: string;
subNotebooks?: Notebook[];
};
export type Item = {

View File

@@ -30,42 +30,28 @@ export async function* iterateList(list: Locator) {
return null;
}
export async function fillNotebookDialog(
page: Page,
notebook: Notebook,
editing = false
) {
const titleInput = page.locator(getTestId("title-input"));
const descriptionInput = page.locator(getTestId("description-input"));
const topicInput = page.locator(getTestId(`edit-topic-input`));
const topicInputAction = page.locator(getTestId(`edit-topic-action`));
export async function fillNotebookDialog(page: Page, notebook: Notebook) {
const dialog = page.locator(getTestId("add-notebook-dialog"));
const titleInput = dialog.locator(getTestId("title-input"));
const descriptionInput = dialog.locator(getTestId("description-input"));
await titleInput.waitFor({ state: "visible" });
await titleInput.fill(notebook.title);
if (notebook.description) await descriptionInput.fill(notebook.description);
const topicItems = page.locator(getTestId("topic-item"));
for (let i = 0; i < notebook.topics.length; ++i) {
if (editing) {
const topicItem = topicItems.nth(i);
await topicItem.click();
}
await topicInput.fill(notebook.topics[i]);
await topicInputAction.click();
}
await confirmDialog(page);
await confirmDialog(dialog);
}
export async function fillReminderDialog(
page: Page,
reminder: Partial<Reminder>
) {
const titleInput = page.locator(getTestId("title-input"));
const descriptionInput = page.locator(getTestId("description-input"));
const dateInput = page.locator(getTestId("date-input"));
const timeInput = page.locator(getTestId("time-input"));
const dialog = page.locator(getTestId("reminder-dialog"));
const titleInput = dialog.locator(getTestId("title-input"));
const descriptionInput = dialog.locator(getTestId("description-input"));
const dateInput = dialog.locator(getTestId("date-input"));
const timeInput = dialog.locator(getTestId("time-input"));
if (reminder.title) {
await titleInput.waitFor({ state: "visible" });
@@ -73,10 +59,10 @@ export async function fillReminderDialog(
}
if (reminder.description) await descriptionInput.fill(reminder.description);
if (reminder.mode)
await page.locator(getTestId(`mode-${reminder.mode}`)).click();
await dialog.locator(getTestId(`mode-${reminder.mode}`)).click();
if (reminder.priority)
await page.locator(getTestId(`priority-${reminder.priority}`)).click();
await dialog.locator(getTestId(`priority-${reminder.priority}`)).click();
if (reminder.recurringMode && reminder.mode === "repeat") {
await page
@@ -89,7 +75,7 @@ export async function fillReminderDialog(
reminder.recurringMode !== "day"
) {
for (const day of reminder.selectedDays) {
await page.locator(getTestId(`day-${day}`)).click();
await dialog.locator(getTestId(`day-${day}`)).click();
}
}
}
@@ -111,25 +97,27 @@ export async function fillReminderDialog(
await timeInput.fill(time);
}
await confirmDialog(page);
await confirmDialog(dialog);
}
export async function fillItemDialog(page: Page, item: Item) {
const titleInput = page.locator(getTestId("title-input"));
const dialog = page.locator(getTestId("item-dialog"));
const titleInput = dialog.locator(getTestId("title-input"));
await titleInput.waitFor({ state: "visible" });
await titleInput.fill(item.title);
await confirmDialog(page);
await confirmDialog(dialog);
}
export async function fillPasswordDialog(page: Page, password: string) {
await page.locator(getTestId("dialog-password")).fill(password);
await confirmDialog(page);
const dialog = page.locator(getTestId("password-dialog"));
await dialog.locator(getTestId("dialog-password")).fill(password);
await confirmDialog(dialog);
}
export async function confirmDialog(page: Page) {
const dialogConfirm = page.locator(getTestId("dialog-yes"));
export async function confirmDialog(dialog: Locator) {
const dialogConfirm = dialog.locator(getTestId("dialog-yes"));
await dialogConfirm.click();
// await dialogConfirm.waitFor({ state: "detached" });
}

View File

@@ -92,12 +92,15 @@ test("add a note to notebook", async ({ page }) => {
await note?.contextMenu.addToNotebook({
title: "Notebook 1",
topics: ["Hello", "World", "Did", "what"]
subNotebooks: [
{ title: "Hello" },
{ title: "World", subNotebooks: [{ title: "Did" }, { title: "what" }] }
]
});
expect(
await app.toasts.waitForToast("1 note added to Hello and 3 others.")
).toBe(true);
expect(await app.toasts.waitForToast("1 note added to 5 notebooks.")).toBe(
true
);
});
const actors = ["contextMenu", "properties"] as const;
@@ -165,7 +168,7 @@ for (const actor of actors) {
await note?.[actor].color("red");
const coloredNotes = await app.goToColor("red");
const coloredNotes = await app.goToColor("Red");
const coloredNote = await coloredNotes.findNote(NOTE);
expect(coloredNote).toBeDefined();
expect(await coloredNote?.contextMenu.isColored("red")).toBe(true);
@@ -307,7 +310,7 @@ test(`sort notes`, async ({ page }, info) => {
});
if (!sortResult) return;
expect(await notes.isEmpty()).toBeFalsy();
await expect(notes.items).toHaveCount(titles.length);
});
}
}

View File

@@ -1,7 +1,3 @@
Test 1
----------
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1
----------
Tags:
This is Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1Test 1

View File

@@ -20,6 +20,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
import { db } from "./db";
import { showPasswordDialog } from "./dialog-controller";
import { showToast } from "../utils/toast";
import { VAULT_ERRORS } from "@notesnook/core/dist/api/vault";
class Vault {
static async createVault() {
@@ -100,9 +101,9 @@ class Vault {
.then(resolve)
.catch(({ message }) => {
switch (message) {
case db.vault.ERRORS.noVault:
case VAULT_ERRORS.noVault:
return Vault.createVault();
case db.vault.ERRORS.vaultLocked:
case VAULT_ERRORS.vaultLocked:
return Vault.unlockVault();
default:
showToast("error", message);

View File

@@ -36,6 +36,7 @@ type DialogButtonProps = ButtonProps & {
};
type DialogProps = SxProp & {
testId?: string;
isOpen?: boolean;
onClose?: (
event?: React.MouseEvent<Element, MouseEvent> | React.KeyboardEvent<Element>
@@ -65,6 +66,9 @@ function BaseDialog(props: React.PropsWithChildren<DialogProps>) {
shouldFocusAfterRender
onAfterOpen={(e) => onAfterOpen(e, props)}
overlayClassName={"theme-scope-dialog"}
data={{
"test-id": props.testId
}}
style={{
content: {
top: 0,

View File

@@ -44,8 +44,17 @@ export type FieldProps = InputProps & {
};
function Field(props: FieldProps) {
const { label, styles, helpText, action, sx, id, type, ...inputProps } =
props;
const {
label,
styles,
helpText,
action,
sx,
id,
type,
inputRef,
...inputProps
} = props;
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
const colorScheme = useThemeStore((state) => state.colorScheme);
@@ -87,6 +96,7 @@ function Field(props: FieldProps) {
<Flex mt={1} sx={{ position: "relative" }}>
<Input
{...inputProps}
ref={inputRef}
id={id}
type={isPasswordVisible ? "text" : type || "text"}
sx={{

View File

@@ -85,7 +85,8 @@ import {
Color,
Note,
Notebook as NotebookItem,
Tag
Tag,
DefaultColors
} from "@notesnook/core";
import { MenuItem } from "@notesnook/ui";
import {
@@ -413,6 +414,7 @@ const menuItems: (
key: "colors",
title: "Assign color",
icon: Colors.path,
multiSelect: true,
menu: { items: colorsToMenuItems(context?.color, ids) }
},
{
@@ -574,14 +576,15 @@ function colorsToMenuItems(
ids: string[]
): MenuItem[] {
return COLORS.map((color) => {
const isChecked = !!noteColor && noteColor.title === color.title;
return {
type: "button",
key: color.key,
title: color.title,
icon: Circle.path,
styles: { icon: { color: StaticColors[color.key] } },
isChecked: noteColor && noteColor.title === color.title,
onClick: () => store.setColor(color.title, ...ids)
styles: { icon: { color: DefaultColors[color.key] } },
isChecked,
onClick: () => store.setColor(color, isChecked, ...ids)
} satisfies MenuItem;
});
}

View File

@@ -204,12 +204,15 @@ function EditorProperties(props: EditorPropertiesProps) {
export default React.memo(EditorProperties);
function Colors({ noteId }: { noteId: string }) {
const result = usePromise(async () =>
(
await db.relations.to({ id: noteId, type: "note" }, "color").resolve(1)
).at(0)
const color = useStore((store) => store.color);
const result = usePromise(
async () =>
(
await db.relations.to({ id: noteId, type: "note" }, "color").resolve(1)
).at(0),
[color]
);
console.log(result);
return (
<Flex
py={2}
@@ -219,38 +222,37 @@ function Colors({ noteId }: { noteId: string }) {
justifyContent: "center"
}}
>
{COLORS.map((label) => (
<Flex
key={label.key}
onClick={() => noteStore.get().setColor(label.key, noteId)}
sx={{
cursor: "pointer",
position: "relative",
alignItems: "center",
justifyContent: "space-between"
}}
data-test-id={`properties-${label}`}
>
<Circle
size={35}
color={DefaultColors[label.key]}
data-test-id={`toggle-state-${
result.status === "fulfilled" &&
label.key === result.value?.colorCode
? "on"
: "off"
}`}
/>
{result.status === "fulfilled" &&
label.key === result.value?.colorCode && (
{COLORS.map((label) => {
const isChecked =
result.status === "fulfilled" &&
DefaultColors[label.key] === result.value?.colorCode;
return (
<Flex
key={label.key}
onClick={() => noteStore.get().setColor(label, isChecked, noteId)}
sx={{
cursor: "pointer",
position: "relative",
alignItems: "center",
justifyContent: "space-between"
}}
data-test-id={`properties-${label.key}`}
>
<Circle
size={35}
color={DefaultColors[label.key]}
data-test-id={`toggle-state-${isChecked ? "on" : "off"}`}
/>
{isChecked && (
<Checkmark
color="white"
size={18}
sx={{ position: "absolute", left: "8px" }}
/>
)}
</Flex>
))}
</Flex>
);
})}
</Flex>
);
}
@@ -271,9 +273,9 @@ function Notebooks({ noteId }: { noteId: string }) {
mode="fixed"
estimatedSize={50}
getItemKey={(index) => result.value.getKey(index)}
items={result.value.ids}
renderItem={(id) => (
<ListItemWrapper id={id as string} items={result.value} simplified />
items={result.value.ungrouped}
renderItem={({ item: id }) => (
<ListItemWrapper id={id} items={result.value} simplified />
)}
/>
</Section>
@@ -294,9 +296,9 @@ function Reminders({ noteId }: { noteId: string }) {
mode="fixed"
estimatedSize={54}
getItemKey={(index) => result.value.getKey(index)}
items={result.value.ids}
renderItem={(id) => (
<ListItemWrapper id={id as string} items={result.value} simplified />
items={result.value.ungrouped}
renderItem={({ item: id }) => (
<ListItemWrapper id={id} items={result.value} simplified />
)}
/>
</Section>
@@ -352,20 +354,18 @@ function SessionHistory({
mode="fixed"
estimatedSize={28}
getItemKey={(index) => result.value.getKey(index)}
items={result.value.ids}
renderItem={(id) => (
<ResolvedItem id={id as string} items={result.value}>
{({ item }) =>
item.type === "session" ? (
<SessionItem
noteId={noteId}
session={item}
dateCreated={dateCreated}
isPreviewMode={isPreviewMode}
onOpenPreviewSession={onOpenPreviewSession}
/>
) : null
}
items={result.value.ungrouped}
renderItem={({ item: id }) => (
<ResolvedItem type="session" id={id} items={result.value}>
{({ item }) => (
<SessionItem
noteId={noteId}
session={item}
dateCreated={dateCreated}
isPreviewMode={isPreviewMode}
onOpenPreviewSession={onOpenPreviewSession}
/>
)}
</ResolvedItem>
)}
/>

View File

@@ -24,7 +24,6 @@ import { store } from "../../stores/trash-store";
import { Flex, Text } from "@theme-ui/components";
import TimeAgo from "../time-ago";
import { pluralize, toTitleCase } from "@notesnook/common";
import { showUndoableToast } from "../../common/toasts";
import { showToast } from "../../utils/toast";
import { hashNavigate } from "../../navigation";
import { useStore } from "../../stores/note-store";
@@ -78,8 +77,8 @@ const menuItems: (item: TrashItem, ids?: string[]) => MenuItem[] = (
key: "restore",
title: "Restore",
icon: Restore.path,
onClick: () => {
store.restore(ids);
onClick: async () => {
await store.restore(...ids);
showToast("success", `${pluralize(ids.length, "item")} restored`);
},
multiSelect: true
@@ -92,11 +91,10 @@ const menuItems: (item: TrashItem, ids?: string[]) => MenuItem[] = (
variant: "dangerous",
onClick: async () => {
if (!(await showMultiPermanentDeleteConfirmation(ids.length))) return;
showUndoableToast(
`${pluralize(ids.length, "item")} permanently deleted`,
() => store.delete(ids),
() => store.delete(ids, true),
() => store.refresh()
await store.delete(...ids);
showToast(
"success",
`${pluralize(ids.length, "item")} permanently deleted`
);
},
multiSelect: true

View File

@@ -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/>.
*/
import { useRef, useState, useCallback, useEffect, useMemo } from "react";
import { useRef, useState, useCallback, useEffect } from "react";
import { Flex, Text, Button } from "@theme-ui/components";
import { Lock } from "../icons";
import { db } from "../../common/db";
@@ -26,6 +26,7 @@ import { useStore as useAppStore } from "../../stores/app-store";
import Field from "../field";
import { showToast } from "../../utils/toast";
import { ErrorText } from "../error-text";
import usePromise from "../../hooks/use-promise";
type UnlockProps = {
noteId: string;
@@ -35,25 +36,23 @@ function Unlock(props: UnlockProps) {
const [isWrong, setIsWrong] = useState(false);
const [isUnlocking, setIsUnlocking] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const passwordRef = useRef<HTMLInputElement>();
const passwordRef = useRef<HTMLInputElement>(null);
const note = useMemo(
() => (!isLoading ? db.notes.note(noteId)?.data : undefined),
[noteId, isLoading]
);
const note = usePromise(() => db.notes.note(noteId), [noteId]);
const openLockedSession = useEditorStore((store) => store.openLockedSession);
const openSession = useEditorStore((store) => store.openSession);
const setIsEditorOpen = useAppStore((store) => store.setIsEditorOpen);
const submit = useCallback(async () => {
console.log("HELO", passwordRef.current);
if (!passwordRef.current) return;
setIsUnlocking(true);
const password = passwordRef.current.value;
try {
if (!password) return;
const note = await db.vault.open(noteId, password);
console.log(note);
openLockedSession(note);
} catch (e) {
if (
@@ -72,12 +71,8 @@ function Unlock(props: UnlockProps) {
useEffect(() => {
(async () => {
setIsLoading(true);
await openSession(noteId);
setIsEditorOpen(true);
setIsLoading(false);
})();
}, [openSession, setIsEditorOpen, noteId]);
@@ -106,7 +101,9 @@ function Unlock(props: UnlockProps) {
mt={25}
sx={{ fontSize: 36, textAlign: "center" }}
>
{note?.title || "Open note"}
{note.status === "fulfilled" && note.value
? note.value.title
: "Open note"}
</Text>
</Flex>
<Text
@@ -129,7 +126,7 @@ function Unlock(props: UnlockProps) {
sx={{ width: ["95%", "95%", "30%"] }}
placeholder="Enter password"
type="password"
onKeyUp={async (e: KeyboardEvent) => {
onKeyUp={async (e) => {
if (e.key === "Enter") {
await submit();
} else if (isWrong) {

View File

@@ -78,6 +78,7 @@ export function VirtualizedList<T>(props: VirtualizedListProps<T>) {
position: "relative",
gap: itemGap
}}
data-test-id="virtualized-list"
>
{virtualItems.map((row) => (
<Box

View File

@@ -70,6 +70,7 @@ function AddNotebookDialog(props: AddNotebookDialogProps) {
}, [props.notebook?.id, props.edit, onClose, parentId]);
return (
<Dialog
testId="add-notebook-dialog"
isOpen={true}
title={props.edit ? "Edit Notebook" : "Create a Notebook"}
description={

View File

@@ -52,6 +52,7 @@ function ConfirmDialog<TCheckId extends string>(
return (
<Dialog
testId="confirm-dialog"
isOpen={true}
title={title}
width={width}

View File

@@ -24,6 +24,7 @@ import Field from "../components/field";
function ItemDialog(props) {
return (
<Dialog
testId="item-dialog"
isOpen={true}
title={props.title}
description={props.subtitle}

View File

@@ -42,7 +42,8 @@ import { Notebook, isGroupHeader } from "@notesnook/core/dist/types";
import {
UncontrolledTreeEnvironment,
Tree,
TreeItemIndex
TreeItemIndex,
TreeEnvironmentRef
} from "react-complex-tree";
import { FlexScrollContainer } from "../components/scroll-container";
import { pluralize } from "@notesnook/common";
@@ -74,6 +75,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
const refreshNotebooks = useStore((store) => store.refresh);
const notebooks = useStore((store) => store.notebooks);
const reloadItem = useRef<(changedItemIds: TreeItemIndex[]) => void>();
const treeRef = useRef<TreeEnvironmentRef>(null);
useEffect(() => {
if (!notebooks) {
@@ -152,6 +154,7 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
return (
<Dialog
testId="move-note-dialog"
isOpen={true}
title={"Select notebooks"}
description={`Use ${
@@ -211,9 +214,10 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
Reset selection
</Button>
)}
{notebooks && (
{notebooks && notebooks.ids.length > 0 ? (
<FlexScrollContainer>
<UncontrolledTreeEnvironment
ref={treeRef}
dataProvider={{
onDidChangeTreeData(listener) {
reloadItem.current = listener;
@@ -300,7 +304,13 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
isExpandable={props.item.isFolder || false}
isExpanded={props.context.isExpanded || false}
toggle={props.context.toggleExpandedState}
onCreateItem={() => reloadItem.current?.([props.item.index])}
onCreateItem={() => {
reloadItem.current?.([props.item.index]);
treeRef.current?.expandItem(
props.item.index,
props.info.treeId
);
}}
/>
{props.children}
@@ -312,6 +322,27 @@ function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
<Tree treeId={"root"} rootItem="root" treeLabel="Tree Example" />
</UncontrolledTreeEnvironment>
</FlexScrollContainer>
) : (
<Flex
sx={{
my: 2,
flexDirection: "column",
justifyContent: "center",
alignItems: "center"
}}
>
<Text variant="body">
Please add a notebook to start linking notes.
</Text>
<Button
data-test-id="add-new-notebook"
variant="secondary"
sx={{ mt: 2 }}
onClick={() => showAddNotebookDialog()}
>
Add new notebook
</Button>
</Flex>
)}
</Dialog>
);
@@ -388,9 +419,17 @@ function NotebookItem(props: {
<Flex sx={{ alignItems: "center" }}>
{isExpandable ? (
isExpanded ? (
<ChevronDown size={20} sx={{ height: "20px" }} />
<ChevronDown
data-test-id="collapse-notebook"
size={20}
sx={{ height: "20px" }}
/>
) : (
<ChevronRight size={20} sx={{ height: "20px" }} />
<ChevronRight
data-test-id="expand-notebook"
size={20}
sx={{ height: "20px" }}
/>
)
) : null}
<SelectedCheck size={20} item={notebook} onClick={check} />
@@ -411,7 +450,7 @@ function NotebookItem(props: {
<TopicSelectionIndicator notebook={notebook} />
<Button
variant="secondary"
data-test-id="create-topic"
data-test-id="add-sub-notebook"
sx={{ p: "small" }}
>
<Plus

View File

@@ -51,6 +51,7 @@ function PasswordDialog(props) {
);
return (
<Dialog
testId="password-dialog"
isOpen={true}
title={props.title}
description={props.subtitle}

View File

@@ -45,6 +45,7 @@ function RecoveryKeyDialog(props) {
return (
<Dialog
testId="recovery-key-dialog"
isOpen={true}
title="Backup your recovery key"
width={400}

View File

@@ -105,6 +105,7 @@ function ReminderDialog(props) {
return (
<Dialog
testId="reminder-dialog"
isOpen={true}
title={reminder.title}
onClose={() => props.onClose(false)}

View File

@@ -30,6 +30,7 @@ export function useIsUserPremium() {
}
export function isUserPremium(user?: User) {
return true;
if (IS_TESTING) return true;
if (!user) user = userstore.get().user;
if (!user) return false;

View File

@@ -71,7 +71,7 @@ export const getDefaultSession = (sessionId?: string): EditorSession => {
class EditorStore extends BaseStore<EditorStore> {
session = getDefaultSession();
color = undefined;
color?: string;
tags: Tag[] = [];
arePropertiesVisible = false;
editorMargins = Config.get("editor:margins", true);
@@ -326,6 +326,8 @@ class EditorStore extends BaseStore<EditorStore> {
name: "favorite" | "pinned" | "readonly" | "localOnly" | "color",
value: boolean | string
) => {
if (name === "color" && typeof value === "string")
return this.set({ color: value });
return this.saveSession(noteId, { [name]: value });
};

View File

@@ -25,7 +25,7 @@ import { store as selectionStore } from "./selection-store";
import Vault from "../common/vault";
import BaseStore from ".";
import Config from "../utils/config";
import { Note, VirtualizedGrouping } from "@notesnook/core";
import { DefaultColors, Note, VirtualizedGrouping } from "@notesnook/core";
import { Context } from "../components/list-container/types";
type ViewMode = "detailed" | "compact";
@@ -129,23 +129,27 @@ class NoteStore extends BaseStore<NoteStore> {
await this.refresh();
};
setColor = async (color: string, ...ids: string[]) => {
// try {
// let note = db.notes.note(id);
// if (!note) return;
// const colorId =
// db.tags.find(color)?.id || (await db.colors.add({ title: color }));
// const isColored =
// db.relations.from({ type: "color", id: colorId }, "note").length > 0;
setColor = async (
color: { key: string; title: string },
isChecked: boolean,
...ids: string[]
) => {
await db.relations.to({ type: "note", ids }, "color").unlink();
if (!isChecked) {
const colorId = await db.colors.add({
title: color.title,
colorCode: DefaultColors[color.key]
});
// if (isColored)
// await db.relations.unlink({ type: "color", id: colorId }, note._note);
// else
// await db.relations.add({ type: "color", id: colorId }, note._note);
// const notes = await db.notes.all.items(ids)
// TODO:
for (const id of ids) {
await db.relations.add(
{ type: "color", id: colorId },
{ type: "note", id }
);
}
}
await appStore.refreshNavItems();
this.syncNoteWithEditor(ids, "color", color);
this.syncNoteWithEditor(ids, "color", color.key);
await this.refresh();
};

View File

@@ -36,6 +36,7 @@ import {
import {
AnyColumnWithTable,
ExpressionOrFactory,
SelectExpression,
SelectQueryBuilder,
SqlBool,
sql
@@ -353,10 +354,29 @@ export class FilteredSelector<T extends Item> {
async grouped(options: GroupOptions) {
console.time("getting items");
const fields: Array<
SelectExpression<DatabaseSchema, keyof DatabaseSchema>
> = ["id", "type", options.sortBy];
if (this.type === "notes") fields.push("notes.pinned", "notes.conflicted");
else if (this.type === "notebooks") fields.push("notebooks.pinned");
else if (this.type === "attachments" && options.groupBy === "abc")
fields.push("attachments.filename");
else if (this.type === "reminders") {
fields.push(
"reminders.mode",
"reminders.date",
"reminders.recurringMode",
"reminders.selectedDays",
"reminders.disabled",
"reminders.snoozeUntil"
);
}
const items = await this.filter
.$if(!!this._limit, (eb) => eb.limit(this._limit))
.$call(this.buildSortExpression(options))
.select(["id", options.sortBy, "type"])
.select(fields)
.execute();
console.timeEnd("getting items");
console.log(items.length);

View File

@@ -60,7 +60,7 @@ function getKeySelector(
const date = new Date();
if (item.type === "reminder")
return "Active"; // isReminderActive(item) ? "Active" : "Inactive";
return isReminderActive(item) ? "Active" : "Inactive";
else if (options.sortBy === "title")
return getFirstCharacter(getTitle(item));
else {