mirror of
https://github.com/streetwriters/notesnook.git
synced 2026-08-29 18:19:38 +02:00
Compare commits
5 Commits
beta
...
move-topic
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b15d5d85f | ||
|
|
171305b83e | ||
|
|
794502ec88 | ||
|
|
284c852031 | ||
|
|
c58c138996 |
@@ -120,7 +120,7 @@ test("pressing Enter should open focused note", async ({ page }) => {
|
||||
expect(await notes.editor.getTitle()).toBe(await notesList[2].getTitle());
|
||||
});
|
||||
|
||||
test.only("pressing Shift+ArrowDown should select next note", async ({ page }) => {
|
||||
test("pressing Shift+ArrowDown should select next note", async ({ page }) => {
|
||||
const { notesList, notes } = await populateList(page);
|
||||
await notes.focus();
|
||||
|
||||
|
||||
@@ -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 { Page } from "@playwright/test";
|
||||
import { Page, Locator } from "@playwright/test";
|
||||
import { getTestId } from "../utils";
|
||||
import { AuthModel } from "./auth.model";
|
||||
import { CheckoutModel } from "./checkout.model";
|
||||
@@ -35,6 +35,7 @@ export class AppModel {
|
||||
readonly navigation: NavigationMenuModel;
|
||||
readonly auth: AuthModel;
|
||||
readonly checkout: CheckoutModel;
|
||||
readonly routeHeader: Locator;
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page;
|
||||
@@ -42,6 +43,7 @@ export class AppModel {
|
||||
this.navigation = new NavigationMenuModel(page);
|
||||
this.auth = new AuthModel(page);
|
||||
this.checkout = new CheckoutModel(page);
|
||||
this.routeHeader = this.page.locator(getTestId("routeHeader"));
|
||||
}
|
||||
|
||||
async goto() {
|
||||
@@ -49,6 +51,10 @@ export class AppModel {
|
||||
await this.getRouteHeader();
|
||||
}
|
||||
|
||||
async goback() {
|
||||
await this.page.locator(getTestId("go-back")).click();
|
||||
}
|
||||
|
||||
async goToNotes() {
|
||||
await this.navigateTo("Notes");
|
||||
return new NotesViewModel(this.page, "home");
|
||||
@@ -85,14 +91,18 @@ export class AppModel {
|
||||
}
|
||||
|
||||
private async navigateTo(title: string) {
|
||||
if ((await this.getRouteHeader()) === title) return;
|
||||
if (
|
||||
!(await this.routeHeader.isVisible()) ||
|
||||
(await this.getRouteHeader()) === title
|
||||
)
|
||||
return;
|
||||
const item = await this.navigation.findItem(title);
|
||||
await item?.click();
|
||||
await this.page.waitForTimeout(1000);
|
||||
}
|
||||
|
||||
getRouteHeader() {
|
||||
return this.page.locator(getTestId("routeHeader")).inputValue();
|
||||
async getRouteHeader() {
|
||||
return await this.page.locator(getTestId("routeHeader")).inputValue();
|
||||
}
|
||||
|
||||
async isSynced() {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { BaseItemModel } from "./base-item.model";
|
||||
import { ContextMenuModel } from "./context-menu.model";
|
||||
import { NotesViewModel } from "./notes-view.model";
|
||||
import { Item } from "./types";
|
||||
import { fillItemDialog } from "./utils";
|
||||
import { fillItemDialog, fillMoveTopicDialog } from "./utils";
|
||||
|
||||
export class ItemModel extends BaseItemModel {
|
||||
private readonly contextMenu: ContextMenuModel;
|
||||
@@ -69,4 +69,10 @@ export class ItemModel extends BaseItemModel {
|
||||
await this.contextMenu.close();
|
||||
return state;
|
||||
}
|
||||
|
||||
async moveItem(notebookTitle: string) {
|
||||
await this.contextMenu.open(this.locator);
|
||||
await this.contextMenu.clickOnItem("move");
|
||||
await fillMoveTopicDialog(this.page, notebookTitle);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,18 @@ export async function fillNotebookDialog(
|
||||
await confirmDialog(page);
|
||||
}
|
||||
|
||||
export async function fillMoveTopicDialog(page: Page, notebookTitle: string) {
|
||||
const notebookList = page.locator(getTestId("notebook-list"));
|
||||
const notebookTitles = notebookList.locator(getTestId("title"));
|
||||
const dialogConfirm = page.locator(getTestId("dialog-yes"));
|
||||
for await (const title of iterateList(notebookTitles)) {
|
||||
if (notebookTitle === (await title.textContent())) {
|
||||
await title.click();
|
||||
}
|
||||
}
|
||||
await confirmDialog(page);
|
||||
}
|
||||
|
||||
export async function fillItemDialog(page: Page, item: Item) {
|
||||
const titleInput = page.locator(getTestId("title-input"));
|
||||
await titleInput.waitFor({ state: "visible" });
|
||||
|
||||
51
apps/web/__e2e__/topics.test.ts
Normal file
51
apps/web/__e2e__/topics.test.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 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 { test, expect } from "@playwright/test";
|
||||
import { AppModel } from "./models/app.model";
|
||||
import { getTestId, NOTE } from "./utils";
|
||||
|
||||
test.only("Move topic to notebook", async ({ page }) => {
|
||||
const NOTEBOOK1 = {
|
||||
title: "Test notebook 1",
|
||||
description: "This is test notebook 1",
|
||||
topics: ["Topic 1", "Very long topic 2", "Topic 3"]
|
||||
};
|
||||
const NOTEBOOK2 = {
|
||||
title: "Test notebook 2",
|
||||
description: "This is test notebook 2",
|
||||
topics: ["Topic 4", "Very long topic 5", "Topic 6"]
|
||||
};
|
||||
|
||||
const app = new AppModel(page);
|
||||
await app.goto();
|
||||
const notebooks = await app.goToNotebooks();
|
||||
const notebook1 = await notebooks.createNotebook(NOTEBOOK1);
|
||||
const notebook2 = await notebooks.createNotebook(NOTEBOOK2);
|
||||
|
||||
const topics = await notebook1?.openNotebook();
|
||||
const topic = await topics?.findItem({ title: NOTEBOOK1.topics[0] });
|
||||
await topic?.moveItem(NOTEBOOK2.title);
|
||||
|
||||
await app.goback();
|
||||
const topics2 = await notebook2?.openNotebook();
|
||||
const topic2 = await topics2?.findItem({ title: NOTEBOOK1.topics[0] });
|
||||
|
||||
expect((await topic2?.getTitle()) === NOTEBOOK1.topics[0]).toBeTruthy();
|
||||
});
|
||||
@@ -350,6 +350,12 @@ export function showMoveNoteDialog(noteIds: string[]) {
|
||||
));
|
||||
}
|
||||
|
||||
export function showMoveTopicsDialog(topics: [], id: String) {
|
||||
return showDialog("MoveTopicsDialog", (Dialog, perform) => (
|
||||
<Dialog topics={topics} id={id} onClose={(res: boolean) => perform(res)} />
|
||||
));
|
||||
}
|
||||
|
||||
function getDialogData(type: string) {
|
||||
switch (type) {
|
||||
case "create_vault":
|
||||
|
||||
@@ -29,6 +29,7 @@ const ImportDialog = React.lazy(() => import("./import-dialog"));
|
||||
const LoadingDialog = React.lazy(() => import("./loading-dialog"));
|
||||
const ProgressDialog = React.lazy(() => import("./progress-dialog"));
|
||||
const MoveDialog = React.lazy(() => import("./move-note-dialog"));
|
||||
const MoveTopicsDialog = React.lazy(() => import("./move-topics-dialog"));
|
||||
const PasswordDialog = React.lazy(() => import("./password-dialog"));
|
||||
const RecoveryKeyDialog = React.lazy(() => import("./recovery-key-dialog"));
|
||||
const ItemDialog = React.lazy(() => import("./item-dialog"));
|
||||
@@ -60,6 +61,7 @@ export const Dialogs = {
|
||||
EmailVerificationDialog,
|
||||
LoadingDialog,
|
||||
MoveDialog,
|
||||
MoveTopicsDialog,
|
||||
PasswordDialog,
|
||||
RecoveryKeyDialog,
|
||||
ItemDialog,
|
||||
|
||||
@@ -17,33 +17,21 @@ 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 {
|
||||
ChangeEvent,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState
|
||||
} from "react";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { Button, Flex, Input, Text } from "@theme-ui/components";
|
||||
import * as Icon from "../icons";
|
||||
import { db } from "../../common/db";
|
||||
import Dialog from "./dialog";
|
||||
import Field from "../field";
|
||||
import { useStore, store } from "../../stores/notebook-store";
|
||||
import { store as notestore } from "../../stores/note-store";
|
||||
import { Perform } from "../../common/dialog-controller";
|
||||
import { ThemeUIStyleObject } from "@theme-ui/core";
|
||||
import { showToast } from "../../utils/toast";
|
||||
import { pluralize } from "../../utils/string";
|
||||
import FilteredList, { Item } from "../filtered-list";
|
||||
|
||||
type MoveDialogProps = { onClose: Perform; noteIds: string[] };
|
||||
type NotebookReference = { id: string; topic: string; type: "add" | "remove" };
|
||||
type Item = {
|
||||
id: string;
|
||||
type: "topic" | "notebook" | "header";
|
||||
title: string;
|
||||
};
|
||||
type Notebook = Item & { topics: Item[] };
|
||||
|
||||
function MoveDialog({ onClose, noteIds }: MoveDialogProps) {
|
||||
@@ -197,109 +185,6 @@ function topicHasNotes(topic: Item, noteIds: string[]) {
|
||||
return noteIds.some((id) => notes.indexOf(id) > -1);
|
||||
}
|
||||
|
||||
type FilteredListProps<T extends Item> = {
|
||||
placeholders: { filter: string; empty: string };
|
||||
items: () => T[];
|
||||
filter: (items: T[], query: string) => T[];
|
||||
onCreateNewItem: (title: string) => Promise<void>;
|
||||
renderItem: (item: T, index: number, refresh: () => void) => JSX.Element;
|
||||
};
|
||||
|
||||
function FilteredList<T extends Item>(props: FilteredListProps<T>) {
|
||||
const {
|
||||
items: _items,
|
||||
filter,
|
||||
onCreateNewItem,
|
||||
placeholders,
|
||||
renderItem
|
||||
} = props;
|
||||
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [query, setQuery] = useState<string>();
|
||||
const noItemsFound = items.length <= 0 && query && query.length > 0;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setItems(_items());
|
||||
}, [_items]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const _filter = useCallback(
|
||||
(query) => {
|
||||
setItems(() => {
|
||||
const items = _items();
|
||||
if (!query) {
|
||||
return items;
|
||||
}
|
||||
return filter(items, query);
|
||||
});
|
||||
setQuery(query);
|
||||
},
|
||||
[_items, filter]
|
||||
);
|
||||
|
||||
const _createNewItem = useCallback(
|
||||
async (title) => {
|
||||
await onCreateNewItem(title);
|
||||
refresh();
|
||||
setQuery(undefined);
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
},
|
||||
[inputRef, refresh, onCreateNewItem]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
inputRef={inputRef}
|
||||
data-test-id={"filter-input"}
|
||||
autoFocus
|
||||
placeholder={
|
||||
items.length <= 0 ? placeholders.empty : placeholders.filter
|
||||
}
|
||||
onChange={(e: ChangeEvent) =>
|
||||
_filter((e.target as HTMLInputElement).value)
|
||||
}
|
||||
onKeyUp={async (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && noItemsFound) {
|
||||
await _createNewItem(query);
|
||||
}
|
||||
}}
|
||||
action={
|
||||
items.length <= 0
|
||||
? {
|
||||
icon: Icon.Plus,
|
||||
onClick: async () => await _createNewItem(query)
|
||||
}
|
||||
: { icon: Icon.Search, onClick: () => _filter(query) }
|
||||
}
|
||||
/>
|
||||
<Flex mt={1} sx={{ overflowY: "hidden", flexDirection: "column" }}>
|
||||
{noItemsFound && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 2
|
||||
}}
|
||||
onClick={async () => {
|
||||
await _createNewItem(query);
|
||||
}}
|
||||
>
|
||||
<Text variant={"body"}>{`Add "${query}"`}</Text>
|
||||
<Icon.Plus size={16} color="primary" />
|
||||
</Button>
|
||||
)}
|
||||
{items.map((item, index) => renderItem(item, index, refresh))}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default MoveDialog;
|
||||
|
||||
type TreeNodeProps<T extends Item> = {
|
||||
|
||||
121
apps/web/src/components/dialogs/move-topics-dialog.tsx
Normal file
121
apps/web/src/components/dialogs/move-topics-dialog.tsx
Normal file
@@ -0,0 +1,121 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 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 { useCallback, useState } from "react";
|
||||
import { Flex, Text } from "@theme-ui/components";
|
||||
import * as Icon from "../icons";
|
||||
import { db } from "../../common/db";
|
||||
import Dialog from "./dialog";
|
||||
import { useStore, store } from "../../stores/notebook-store";
|
||||
import { Perform } from "../../common/dialog-controller";
|
||||
import FilteredList, { Item } from "../filtered-list";
|
||||
|
||||
type MoveDialogProps = { onClose: Perform; topics: []; id: String };
|
||||
type Notebook = Item;
|
||||
|
||||
function MoveDialog({ onClose, topics, id }: MoveDialogProps) {
|
||||
const [selected, setSelected] = useState(String);
|
||||
|
||||
const refreshNotebooks = useStore((store) => store.refresh);
|
||||
const getAllNotebooks = useCallback(() => {
|
||||
refreshNotebooks();
|
||||
return (store.get().notebooks as Notebook[]).filter((a) => {
|
||||
return a.type !== "header" && a.id !== id;
|
||||
});
|
||||
}, [refreshNotebooks]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
isOpen={true}
|
||||
title={"Move topic(s)"}
|
||||
description={"You can move topics between notebooks"}
|
||||
onClose={onClose}
|
||||
width={"30%"}
|
||||
positiveButton={{
|
||||
text: "Finish",
|
||||
disabled: !selected.length,
|
||||
onClick: async () => {
|
||||
await db?.notebooks?.moveTopics(selected, topics);
|
||||
refreshNotebooks();
|
||||
onClose(true);
|
||||
}
|
||||
}}
|
||||
negativeButton={{
|
||||
text: "Cancel",
|
||||
onClick: onClose
|
||||
}}
|
||||
>
|
||||
<Flex
|
||||
mt={1}
|
||||
sx={{ overflowY: "hidden", flexDirection: "column" }}
|
||||
data-test-id="notebook-list"
|
||||
>
|
||||
<FilteredList
|
||||
placeholders={{
|
||||
empty: "Add a new notebook",
|
||||
filter: "Filter notebooks"
|
||||
}}
|
||||
items={getAllNotebooks}
|
||||
filter={(notebooks, query) =>
|
||||
db.lookup?.notebooks(notebooks, query) || []
|
||||
}
|
||||
onCreateNewItem={async (title) =>
|
||||
await db.notebooks?.add({
|
||||
title
|
||||
})
|
||||
}
|
||||
renderItem={(notebook, _index, refresh) => {
|
||||
return (
|
||||
<Flex
|
||||
sx={{
|
||||
alignItems: "center",
|
||||
p: "3px",
|
||||
cursor: "pointer",
|
||||
":hover": { bg: "hover" },
|
||||
justifyContent: "space-between"
|
||||
}}
|
||||
onClick={(e) => {
|
||||
setSelected(notebook.id);
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
<Flex>
|
||||
{selected === notebook.id ? (
|
||||
<Icon.Checkmark size={18} />
|
||||
) : (
|
||||
<Icon.ChevronRight size={18} />
|
||||
)}
|
||||
<Text
|
||||
variant={"body"}
|
||||
sx={{ fontSize: "subtitle" }}
|
||||
data-test-id="title"
|
||||
>
|
||||
{notebook.title}
|
||||
</Text>
|
||||
</Flex>
|
||||
</Flex>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Flex>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
export default MoveDialog;
|
||||
135
apps/web/src/components/filtered-list/index.tsx
Normal file
135
apps/web/src/components/filtered-list/index.tsx
Normal file
@@ -0,0 +1,135 @@
|
||||
/*
|
||||
This file is part of the Notesnook project (https://notesnook.com/)
|
||||
|
||||
Copyright (C) 2022 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 { ChangeEvent, useCallback, useEffect, useRef, useState } from "react";
|
||||
import { Button, Flex, Text } from "@theme-ui/components";
|
||||
import * as Icon from "../icons";
|
||||
import Field from "../field";
|
||||
|
||||
type FilteredListProps<T extends Item> = {
|
||||
placeholders: { filter: string; empty: string };
|
||||
items: () => T[];
|
||||
filter: (items: T[], query: string) => T[];
|
||||
onCreateNewItem: (title: string) => Promise<void>;
|
||||
renderItem: (item: T, index: number, refresh: () => void) => JSX.Element;
|
||||
};
|
||||
|
||||
export type Item = {
|
||||
id: string;
|
||||
type: "topic" | "notebook" | "header";
|
||||
title: string;
|
||||
};
|
||||
|
||||
function FilteredList<T extends Item>(props: FilteredListProps<T>) {
|
||||
const {
|
||||
items: _items,
|
||||
filter,
|
||||
onCreateNewItem,
|
||||
placeholders,
|
||||
renderItem
|
||||
} = props;
|
||||
|
||||
const [items, setItems] = useState<T[]>([]);
|
||||
const [query, setQuery] = useState<string>();
|
||||
const noItemsFound = items.length <= 0 && query && query.length > 0;
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
|
||||
const refresh = useCallback(() => {
|
||||
setItems(_items());
|
||||
}, [_items]);
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [refresh]);
|
||||
|
||||
const _filter = useCallback(
|
||||
(query) => {
|
||||
setItems(() => {
|
||||
const items = _items();
|
||||
if (!query) {
|
||||
return items;
|
||||
}
|
||||
return filter(items, query);
|
||||
});
|
||||
setQuery(query);
|
||||
},
|
||||
[_items, filter]
|
||||
);
|
||||
|
||||
const _createNewItem = useCallback(
|
||||
async (title) => {
|
||||
await onCreateNewItem(title);
|
||||
refresh();
|
||||
setQuery(undefined);
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
},
|
||||
[inputRef, refresh, onCreateNewItem]
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Field
|
||||
inputRef={inputRef}
|
||||
data-test-id={"filter-input"}
|
||||
autoFocus
|
||||
placeholder={
|
||||
items.length <= 0 ? placeholders.empty : placeholders.filter
|
||||
}
|
||||
onChange={(e: ChangeEvent) =>
|
||||
_filter((e.target as HTMLInputElement).value)
|
||||
}
|
||||
onKeyUp={async (e: KeyboardEvent) => {
|
||||
if (e.key === "Enter" && noItemsFound) {
|
||||
await _createNewItem(query);
|
||||
}
|
||||
}}
|
||||
action={
|
||||
items.length <= 0
|
||||
? {
|
||||
icon: Icon.Plus,
|
||||
onClick: async () => await _createNewItem(query)
|
||||
}
|
||||
: { icon: Icon.Search, onClick: () => _filter(query) }
|
||||
}
|
||||
/>
|
||||
<Flex mt={1} sx={{ overflowY: "hidden", flexDirection: "column" }}>
|
||||
{noItemsFound && (
|
||||
<Button
|
||||
variant={"secondary"}
|
||||
sx={{
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
py: 2
|
||||
}}
|
||||
onClick={async () => {
|
||||
await _createNewItem(query);
|
||||
}}
|
||||
>
|
||||
<Text variant={"body"}>{`Add "${query}"`}</Text>
|
||||
<Icon.Plus size={16} color="primary" />
|
||||
</Button>
|
||||
)}
|
||||
{items.map((item, index) => renderItem(item, index, refresh))}
|
||||
</Flex>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default FilteredList;
|
||||
@@ -247,6 +247,7 @@ function GroupHeader(props) {
|
||||
{index === 0 && (
|
||||
<Flex mr={1}>
|
||||
<IconButton
|
||||
testId={"sort-icon-button"}
|
||||
icon={
|
||||
groupOptions.sortDirection === "asc"
|
||||
? Icon.SortAsc
|
||||
@@ -292,6 +293,7 @@ function IconButton(props) {
|
||||
const isMobile = useMobile();
|
||||
return (
|
||||
<Button
|
||||
data-test-id={props.testId}
|
||||
variant="secondary"
|
||||
bg="transparent"
|
||||
title={title}
|
||||
|
||||
@@ -26,6 +26,7 @@ import { Flex, Text } from "@theme-ui/components";
|
||||
import * as Icon from "../icons";
|
||||
import { Multiselect } from "../../common/multi-select";
|
||||
import { pluralize } from "../../utils/string";
|
||||
import { showMoveTopicsDialog } from "../../common/dialog-controller";
|
||||
|
||||
function Topic({ item, index, onClick }) {
|
||||
const { id, notebookId } = item;
|
||||
@@ -87,6 +88,15 @@ const menuItems = [
|
||||
icon: Icon.Shortcut,
|
||||
onClick: ({ topic }) => appStore.addToShortcuts(topic)
|
||||
},
|
||||
{
|
||||
key: "move",
|
||||
title: "Move Topic",
|
||||
icon: Icon.Move,
|
||||
onClick: ({ items, notebookId }) => {
|
||||
showMoveTopicsDialog(items, notebookId);
|
||||
},
|
||||
multiSelect: true
|
||||
},
|
||||
{
|
||||
key: "delete",
|
||||
title: "Delete",
|
||||
|
||||
@@ -155,4 +155,25 @@ export default class Notebooks extends Collection {
|
||||
await this._db.trash.add(notebookData);
|
||||
}
|
||||
}
|
||||
|
||||
async moveTopics(notebookId, topics) {
|
||||
if (!notebookId)
|
||||
throw new Error("The destination notebook cannot be undefined.");
|
||||
if (!topics.length) throw new Error("You must select one or more topics");
|
||||
|
||||
for (const topic of topics) {
|
||||
let notesInTopic = [...this._db.notes.topicReferences.get(topic.id)];
|
||||
await this._db.notes.addToNotebook(
|
||||
{ id: notebookId, topic: topic.id },
|
||||
...notesInTopic
|
||||
);
|
||||
await this._db.notebooks
|
||||
.notebook(topic.notebookId)
|
||||
.topics.delete(topic.id);
|
||||
|
||||
topic.notebookId = notebookId;
|
||||
}
|
||||
|
||||
await this._db.notebooks.notebook(notebookId).topics.add(...topics);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -55,10 +55,11 @@ export default class Topic {
|
||||
clear() {
|
||||
const noteIds = this._db.notes.topicReferences.get(this.id);
|
||||
if (!noteIds.length) return;
|
||||
|
||||
return this._db.notes.deleteFromNotebook(
|
||||
this._notebookId,
|
||||
this.id,
|
||||
return this._db.notes.removeFromNotebook(
|
||||
{
|
||||
id: this._notebookId,
|
||||
topic: this.id
|
||||
},
|
||||
...noteIds
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user