mirror of
https://github.com/streetwriters/notesnook.git
synced 2025-12-23 06:59:31 +01:00
web: add move notebook dialog (#8099)
Signed-off-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> Co-authored-by: Abdullah Atta <abdullahatta@streetwriters.co>
This commit is contained in:
@@ -19,7 +19,7 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import ListItem from "../list-item";
|
import ListItem from "../list-item";
|
||||||
import { Button, Flex, Text } from "@theme-ui/components";
|
import { Button, Flex, Text } from "@theme-ui/components";
|
||||||
import { store, useStore as useNotesStore } from "../../stores/note-store";
|
import { useStore as useNotesStore } from "../../stores/note-store";
|
||||||
import { Notebook as NotebookType } from "@notesnook/core";
|
import { Notebook as NotebookType } from "@notesnook/core";
|
||||||
import {
|
import {
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
@@ -30,7 +30,8 @@ import {
|
|||||||
Shortcut,
|
Shortcut,
|
||||||
Trash,
|
Trash,
|
||||||
Notebook as NotebookIcon,
|
Notebook as NotebookIcon,
|
||||||
ArrowUp
|
ArrowUp,
|
||||||
|
Move
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { MenuItem } from "@notesnook/ui";
|
import { MenuItem } from "@notesnook/ui";
|
||||||
import { hashNavigate, navigate } from "../../navigation";
|
import { hashNavigate, navigate } from "../../navigation";
|
||||||
@@ -45,6 +46,7 @@ import { strings } from "@notesnook/intl";
|
|||||||
import { db } from "../../common/db";
|
import { db } from "../../common/db";
|
||||||
import { createSetDefaultHomepageMenuItem } from "../../common";
|
import { createSetDefaultHomepageMenuItem } from "../../common";
|
||||||
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
import { useStore as useNotebookStore } from "../../stores/notebook-store";
|
||||||
|
import { MoveNotebookDialog } from "../../dialogs/move-notebook-dialog";
|
||||||
|
|
||||||
type NotebookProps = {
|
type NotebookProps = {
|
||||||
item: NotebookType;
|
item: NotebookType;
|
||||||
@@ -232,6 +234,15 @@ export const notebookMenuItems: (
|
|||||||
onClick: () => appStore.addToShortcuts(notebook)
|
onClick: () => appStore.addToShortcuts(notebook)
|
||||||
},
|
},
|
||||||
{ key: "sep1", type: "separator" },
|
{ key: "sep1", type: "separator" },
|
||||||
|
{
|
||||||
|
type: "button",
|
||||||
|
key: "move",
|
||||||
|
icon: Move.path,
|
||||||
|
title: strings.move(),
|
||||||
|
onClick: () => {
|
||||||
|
MoveNotebookDialog.show({ notebook: notebook });
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
key: "move-to-top",
|
key: "move-to-top",
|
||||||
@@ -255,7 +266,7 @@ export const notebookMenuItems: (
|
|||||||
},
|
},
|
||||||
multiSelect: false
|
multiSelect: false
|
||||||
},
|
},
|
||||||
{ key: "sep2", type: "separator", isHidden: context?.isRoot },
|
{ key: "sep2", type: "separator" },
|
||||||
{
|
{
|
||||||
type: "button",
|
type: "button",
|
||||||
key: "movetotrash",
|
key: "movetotrash",
|
||||||
|
|||||||
@@ -19,37 +19,22 @@ along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|||||||
|
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Flex, Text } from "@theme-ui/components";
|
import { Flex, Text } from "@theme-ui/components";
|
||||||
import {
|
|
||||||
CheckCircleOutline,
|
|
||||||
CheckRemove,
|
|
||||||
CircleEmpty
|
|
||||||
} from "../components/icons";
|
|
||||||
import { db } from "../common/db";
|
import { db } from "../common/db";
|
||||||
import Dialog from "../components/dialog";
|
import Dialog from "../components/dialog";
|
||||||
import { useStore, store } from "../stores/tag-store";
|
import { useStore, store } from "../stores/tag-store";
|
||||||
import { store as notestore } from "../stores/note-store";
|
import { store as notestore } from "../stores/note-store";
|
||||||
import { FilteredList } from "../components/filtered-list";
|
import { FilteredList } from "../components/filtered-list";
|
||||||
import { ItemReference, Tag } from "@notesnook/core";
|
import { ItemReference, Tag } from "@notesnook/core";
|
||||||
import { create } from "zustand";
|
|
||||||
import { VirtualizedGrouping } from "@notesnook/core";
|
import { VirtualizedGrouping } from "@notesnook/core";
|
||||||
import { ResolvedItem } from "@notesnook/common";
|
import { ResolvedItem } from "@notesnook/common";
|
||||||
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||||
import { strings } from "@notesnook/intl";
|
import { strings } from "@notesnook/intl";
|
||||||
|
import {
|
||||||
type SelectedReference = {
|
SelectedCheck,
|
||||||
id: string;
|
SelectedReference,
|
||||||
new: boolean;
|
selectMultiple,
|
||||||
op: "add" | "remove";
|
useSelectionStore
|
||||||
};
|
} from "./move-note-dialog";
|
||||||
|
|
||||||
interface ISelectionStore {
|
|
||||||
selected: SelectedReference[];
|
|
||||||
setSelected(refs: SelectedReference[]): void;
|
|
||||||
}
|
|
||||||
export const useSelectionStore = create<ISelectionStore>((set) => ({
|
|
||||||
selected: [],
|
|
||||||
setSelected: (selected) => set({ selected: selected.slice() })
|
|
||||||
}));
|
|
||||||
|
|
||||||
type AddTagsDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
|
type AddTagsDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
|
||||||
export const AddTagsDialog = DialogManager.register(function AddTagsDialog(
|
export const AddTagsDialog = DialogManager.register(function AddTagsDialog(
|
||||||
@@ -91,8 +76,9 @@ export const AddTagsDialog = DialogManager.register(function AddTagsDialog(
|
|||||||
positiveButton={{
|
positiveButton={{
|
||||||
text: strings.done(),
|
text: strings.done(),
|
||||||
onClick: async () => {
|
onClick: async () => {
|
||||||
|
const { selected } = useSelectionStore.getState();
|
||||||
for (const id of noteIds) {
|
for (const id of noteIds) {
|
||||||
for (const item of useSelectionStore.getState().selected) {
|
for (const item of selected) {
|
||||||
const tagRef: ItemReference = { type: "tag", id: item.id };
|
const tagRef: ItemReference = { type: "tag", id: item.id };
|
||||||
const noteRef: ItemReference = { id, type: "note" };
|
const noteRef: ItemReference = { id, type: "note" };
|
||||||
if (item.op === "add") await db.relations.add(tagRef, noteRef);
|
if (item.op === "add") await db.relations.add(tagRef, noteRef);
|
||||||
@@ -166,47 +152,15 @@ function TagItem(props: { tag: Tag }) {
|
|||||||
}}
|
}}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const { selected, setSelected } = useSelectionStore.getState();
|
const { selected, setSelected } = useSelectionStore.getState();
|
||||||
|
setSelected(selectMultiple(tag, selected));
|
||||||
const copy = selected.slice();
|
|
||||||
const index = copy.findIndex((item) => item.id === tag.id);
|
|
||||||
const isNew = copy[index] && copy[index].new;
|
|
||||||
if (isNew) {
|
|
||||||
copy.splice(index, 1);
|
|
||||||
} else if (index > -1) {
|
|
||||||
copy[index] = {
|
|
||||||
...copy[index],
|
|
||||||
op: copy[index].op === "add" ? "remove" : "add"
|
|
||||||
};
|
|
||||||
} else {
|
|
||||||
copy.push({ id: tag.id, new: true, op: "add" });
|
|
||||||
}
|
|
||||||
setSelected(copy);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Flex sx={{ alignItems: "center" }}>
|
<Flex sx={{ alignItems: "center" }}>
|
||||||
<SelectedCheck size={20} id={tag.id} />
|
<SelectedCheck size={18} item={tag} />
|
||||||
<Text
|
<Text className="title" data-test-id="tag-title" variant="body">
|
||||||
className="title"
|
|
||||||
data-test-id="notebook-title"
|
|
||||||
variant="subtitle"
|
|
||||||
sx={{ fontWeight: "body", color: "paragraph" }}
|
|
||||||
>
|
|
||||||
#{tag.title}
|
#{tag.title}
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function SelectedCheck({ id, size = 20 }: { id: string; size?: number }) {
|
|
||||||
const selected = useSelectionStore((store) => store.selected);
|
|
||||||
const selectedTag = selected.find((item) => item.id === id);
|
|
||||||
|
|
||||||
return selectedTag?.op === "add" ? (
|
|
||||||
<CheckCircleOutline size={size} sx={{ mr: 1 }} color="accent" />
|
|
||||||
) : selectedTag?.op === "remove" ? (
|
|
||||||
<CheckRemove size={size} sx={{ mr: 1 }} color="icon-error" />
|
|
||||||
) : (
|
|
||||||
<CircleEmpty size={size} sx={{ mr: 1, opacity: 0.4 }} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -49,16 +49,16 @@ import {
|
|||||||
} from "../components/virtualized-tree";
|
} from "../components/virtualized-tree";
|
||||||
|
|
||||||
type MoveNoteDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
|
type MoveNoteDialogProps = BaseDialogProps<boolean> & { noteIds: string[] };
|
||||||
type NotebookReference = {
|
export type SelectedReference = {
|
||||||
id: string;
|
id: string;
|
||||||
new: boolean;
|
new: boolean;
|
||||||
op: "add" | "remove";
|
op: "add" | "remove";
|
||||||
};
|
};
|
||||||
|
|
||||||
interface ISelectionStore {
|
interface ISelectionStore {
|
||||||
selected: NotebookReference[];
|
selected: SelectedReference[];
|
||||||
isMultiselect: boolean;
|
isMultiselect: boolean;
|
||||||
setSelected(refs: NotebookReference[]): void;
|
setSelected(refs: SelectedReference[]): void;
|
||||||
setIsMultiselect(state: boolean): void;
|
setIsMultiselect(state: boolean): void;
|
||||||
}
|
}
|
||||||
export const useSelectionStore = create<ISelectionStore>((set) => ({
|
export const useSelectionStore = create<ISelectionStore>((set) => ({
|
||||||
@@ -87,7 +87,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async function () {
|
(async function () {
|
||||||
const selected: NotebookReference[] = useSelectionStore
|
const selected: SelectedReference[] = useSelectionStore
|
||||||
.getState()
|
.getState()
|
||||||
.selected.slice();
|
.selected.slice();
|
||||||
|
|
||||||
@@ -201,7 +201,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
|
|||||||
<Button
|
<Button
|
||||||
variant="anchor"
|
variant="anchor"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const originalSelection: NotebookReference[] = useSelectionStore
|
const originalSelection: SelectedReference[] = useSelectionStore
|
||||||
.getState()
|
.getState()
|
||||||
.selected.filter((a) => !a.new)
|
.selected.filter((a) => !a.new)
|
||||||
.map((s) => ({ ...s, op: "add" }));
|
.map((s) => ({ ...s, op: "add" }));
|
||||||
@@ -268,6 +268,7 @@ export const MoveNoteDialog = DialogManager.register(function MoveNoteDialog({
|
|||||||
expand: true
|
expand: true
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
multiSelectable
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -310,20 +311,28 @@ function calculateIndentation(
|
|||||||
depth: number,
|
depth: number,
|
||||||
base: number
|
base: number
|
||||||
) {
|
) {
|
||||||
if (expandable && depth > 0) return depth * 7 + base;
|
if (expandable && depth > 0) return depth * 18 + base;
|
||||||
else if (depth === 0) return 0;
|
else if (depth === 0) return 0;
|
||||||
else return depth * 12 + base;
|
else return depth * 24 + base;
|
||||||
}
|
}
|
||||||
function NotebookItem(props: {
|
export function NotebookItem(props: {
|
||||||
notebook: Notebook;
|
notebook: Notebook;
|
||||||
isExpanded: boolean;
|
isExpanded: boolean;
|
||||||
isExpandable: boolean;
|
isExpandable: boolean;
|
||||||
toggle: () => void;
|
toggle: () => void;
|
||||||
depth: number;
|
depth: number;
|
||||||
onCreateItem: () => void;
|
onCreateItem: () => void;
|
||||||
|
multiSelectable?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const { notebook, isExpanded, toggle, depth, isExpandable, onCreateItem } =
|
const {
|
||||||
props;
|
notebook,
|
||||||
|
isExpanded,
|
||||||
|
toggle,
|
||||||
|
depth,
|
||||||
|
isExpandable,
|
||||||
|
onCreateItem,
|
||||||
|
multiSelectable
|
||||||
|
} = props;
|
||||||
|
|
||||||
const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect);
|
const setIsMultiselect = useSelectionStore((store) => store.setIsMultiselect);
|
||||||
const setSelected = useSelectionStore((store) => store.setSelected);
|
const setSelected = useSelectionStore((store) => store.setSelected);
|
||||||
@@ -339,13 +348,13 @@ function NotebookItem(props: {
|
|||||||
const isCtrlPressed = e.ctrlKey || e.metaKey;
|
const isCtrlPressed = e.ctrlKey || e.metaKey;
|
||||||
if (isCtrlPressed) setIsMultiselect(true);
|
if (isCtrlPressed) setIsMultiselect(true);
|
||||||
|
|
||||||
if (isMultiselect || isCtrlPressed) {
|
if (multiSelectable && (isMultiselect || isCtrlPressed)) {
|
||||||
setSelected(selectMultiple(notebook, selected));
|
setSelected(selectMultiple(notebook, selected));
|
||||||
} else {
|
} else {
|
||||||
setSelected(selectSingle(notebook, selected));
|
setSelected(selectSingle(notebook, selected));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isMultiselect, notebook, setIsMultiselect, setSelected]
|
[isMultiselect, multiSelectable, notebook, setIsMultiselect, setSelected]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -377,35 +386,17 @@ function NotebookItem(props: {
|
|||||||
<Flex sx={{ alignItems: "center" }}>
|
<Flex sx={{ alignItems: "center" }}>
|
||||||
{isExpandable ? (
|
{isExpandable ? (
|
||||||
isExpanded ? (
|
isExpanded ? (
|
||||||
<ChevronDown
|
<ChevronDown data-test-id="collapse-notebook" size={18} />
|
||||||
data-test-id="collapse-notebook"
|
|
||||||
size={20}
|
|
||||||
sx={{ height: "20px" }}
|
|
||||||
/>
|
|
||||||
) : (
|
) : (
|
||||||
<ChevronRight
|
<ChevronRight data-test-id="expand-notebook" size={18} />
|
||||||
data-test-id="expand-notebook"
|
|
||||||
size={20}
|
|
||||||
sx={{ height: "20px" }}
|
|
||||||
/>
|
|
||||||
)
|
)
|
||||||
) : null}
|
) : null}
|
||||||
<SelectedCheck size={20} item={notebook} onClick={check} />
|
<SelectedCheck size={18} item={notebook} onClick={check} />
|
||||||
<Text
|
<Text className="title" data-test-id="notebook-title" variant="body">
|
||||||
className="title"
|
|
||||||
data-test-id="notebook-title"
|
|
||||||
variant="subtitle"
|
|
||||||
sx={{ fontWeight: "body" }}
|
|
||||||
>
|
|
||||||
{notebook.title}
|
{notebook.title}
|
||||||
{/* <Text variant="subBody" sx={{ fontWeight: "body" }}>
|
|
||||||
{" "}
|
|
||||||
({pluralize(notebook.topics.length, "topic")})
|
|
||||||
</Text> */}
|
|
||||||
</Text>
|
</Text>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Flex data-test-id="notebook-tools" sx={{ alignItems: "center" }}>
|
<Flex data-test-id="notebook-tools" sx={{ alignItems: "center" }}>
|
||||||
<TopicSelectionIndicator notebook={notebook} />
|
|
||||||
<Button
|
<Button
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
data-test-id="add-sub-notebook"
|
data-test-id="add-sub-notebook"
|
||||||
@@ -426,46 +417,27 @@ function NotebookItem(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TopicSelectionIndicator({ notebook }: { notebook: Notebook }) {
|
export function SelectedCheck({
|
||||||
const hasSelectedTopics = useSelectionStore(
|
|
||||||
(store) => store.selected.filter((nb) => nb.id === notebook.id).length > 0
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!hasSelectedTopics) return null;
|
|
||||||
return <Circle size={8} color="accent" sx={{ mr: 1 }} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SelectedCheck({
|
|
||||||
item,
|
item,
|
||||||
size = 20,
|
size = 20,
|
||||||
onClick
|
onClick
|
||||||
}: {
|
}: {
|
||||||
item?: Notebook;
|
item?: { id: string };
|
||||||
size?: number;
|
size?: number;
|
||||||
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
onClick?: React.MouseEventHandler<HTMLDivElement>;
|
||||||
}) {
|
}) {
|
||||||
const selectedItems = useSelectionStore((store) => store.selected);
|
const selectedItems = useSelectionStore((store) => store.selected);
|
||||||
|
|
||||||
const selectedItem =
|
const selectedItem =
|
||||||
item && selectedItems[findSelectionIndex(item, selectedItems)];
|
item && selectedItems[findSelectionIndex(item, selectedItems)];
|
||||||
const selected =
|
|
||||||
selectedItem?.op === "remove" ? "remove" : selectedItem?.op === "add";
|
|
||||||
|
|
||||||
return selected === true ? (
|
return selectedItem?.op === "add" ? (
|
||||||
<CheckCircleOutline
|
<CheckCircleOutline
|
||||||
size={size}
|
size={size}
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
color="accent"
|
color="accent"
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
) : selected === null ? (
|
) : selectedItem?.op === "remove" ? (
|
||||||
<CheckIntermediate
|
|
||||||
size={size}
|
|
||||||
sx={{ mr: 1 }}
|
|
||||||
color="var(--accent-secondary)"
|
|
||||||
onClick={onClick}
|
|
||||||
/>
|
|
||||||
) : selected === "remove" ? (
|
|
||||||
<CheckRemove
|
<CheckRemove
|
||||||
size={size}
|
size={size}
|
||||||
sx={{ mr: 1 }}
|
sx={{ mr: 1 }}
|
||||||
@@ -477,18 +449,15 @@ function SelectedCheck({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSelection(notebook: Notebook): NotebookReference {
|
function createSelection(item: { id: string }): SelectedReference {
|
||||||
return {
|
return {
|
||||||
id: notebook.id,
|
id: item.id,
|
||||||
op: "add",
|
op: "add",
|
||||||
new: true
|
new: true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function findSelectionIndex(
|
function findSelectionIndex(ref: { id: string }, array: { id: string }[]) {
|
||||||
ref: NotebookReference | Notebook,
|
|
||||||
array: NotebookReference[]
|
|
||||||
) {
|
|
||||||
return array.findIndex((a) => a.id === ref.id);
|
return array.findIndex((a) => a.id === ref.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,7 +467,10 @@ function notebookHasNotes(notebookId: string, noteIds: string[]) {
|
|||||||
.has(...noteIds);
|
.has(...noteIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectMultiple(topic: Notebook, selected: NotebookReference[]) {
|
export function selectMultiple(
|
||||||
|
topic: { id: string },
|
||||||
|
selected: SelectedReference[]
|
||||||
|
) {
|
||||||
const index = findSelectionIndex(topic, selected);
|
const index = findSelectionIndex(topic, selected);
|
||||||
const isSelected = index > -1;
|
const isSelected = index > -1;
|
||||||
const item = selected[index];
|
const item = selected[index];
|
||||||
@@ -514,8 +486,8 @@ function selectMultiple(topic: Notebook, selected: NotebookReference[]) {
|
|||||||
return selected;
|
return selected;
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectSingle(topic: Notebook, array: NotebookReference[]) {
|
function selectSingle(topic: { id: string }, array: SelectedReference[]) {
|
||||||
const selected: NotebookReference[] = array.filter((ref) => !ref.new);
|
const selected: SelectedReference[] = array.filter((ref) => !ref.new);
|
||||||
|
|
||||||
const index = findSelectionIndex(topic, array);
|
const index = findSelectionIndex(topic, array);
|
||||||
const item = array[index];
|
const item = array[index];
|
||||||
|
|||||||
255
apps/web/src/dialogs/move-notebook-dialog.tsx
Normal file
255
apps/web/src/dialogs/move-notebook-dialog.tsx
Normal file
@@ -0,0 +1,255 @@
|
|||||||
|
/*
|
||||||
|
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 { Notebook } from "@notesnook/core";
|
||||||
|
import { strings } from "@notesnook/intl";
|
||||||
|
import { Button, Flex, Text } from "@theme-ui/components";
|
||||||
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
|
import { db } from "../common/db";
|
||||||
|
import { BaseDialogProps, DialogManager } from "../common/dialog-manager";
|
||||||
|
import Dialog from "../components/dialog";
|
||||||
|
import Field from "../components/field";
|
||||||
|
import {
|
||||||
|
TreeNode,
|
||||||
|
VirtualizedTree,
|
||||||
|
VirtualizedTreeHandle
|
||||||
|
} from "../components/virtualized-tree";
|
||||||
|
import { store as notebookStore, useStore } from "../stores/notebook-store";
|
||||||
|
import { AddNotebookDialog } from "./add-notebook-dialog";
|
||||||
|
import { NotebookItem, useSelectionStore } from "./move-note-dialog";
|
||||||
|
|
||||||
|
type MoveNotebookDialogProps = BaseDialogProps<boolean> & {
|
||||||
|
notebook: Notebook;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MoveNotebookDialog = DialogManager.register(
|
||||||
|
function MoveNotebookDialog({ onClose, notebook }: MoveNotebookDialogProps) {
|
||||||
|
const setSelected = useSelectionStore((store) => store.setSelected);
|
||||||
|
const refreshNotebooks = useStore((store) => store.refresh);
|
||||||
|
const treeRef = useRef<VirtualizedTreeHandle<Notebook>>(null);
|
||||||
|
const [notebooks, setNotebooks] = useState<string[]>([]);
|
||||||
|
const notebookId = notebook.id;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
db.notebooks.roots
|
||||||
|
.ids(db.settings.getGroupOptions("notebooks"))
|
||||||
|
.then((ids) => setNotebooks(ids));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async function () {
|
||||||
|
const parentNotebookId = await getParentNotebookId(notebookId);
|
||||||
|
const selected = useSelectionStore.getState().selected[0];
|
||||||
|
if (!selected && parentNotebookId) {
|
||||||
|
setSelected([{ id: parentNotebookId, new: false, op: "add" }]);
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}, [notebookId, refreshNotebooks, setSelected]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
treeRef.current?.refresh();
|
||||||
|
}, [notebooks]);
|
||||||
|
|
||||||
|
const _onClose = useCallback(
|
||||||
|
(result: boolean) => {
|
||||||
|
setSelected([]);
|
||||||
|
onClose(result);
|
||||||
|
},
|
||||||
|
[setSelected, onClose]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
testId="move-notebook-dialog"
|
||||||
|
isOpen={true}
|
||||||
|
title={`${strings.move()} ${notebook.title}`}
|
||||||
|
description={strings.moveNotebookDesc()}
|
||||||
|
onClose={() => _onClose(false)}
|
||||||
|
width={500}
|
||||||
|
noScroll
|
||||||
|
positiveButton={{
|
||||||
|
text: strings.done(),
|
||||||
|
onClick: async () => {
|
||||||
|
const { selected } = useSelectionStore.getState();
|
||||||
|
for (const item of selected) {
|
||||||
|
if (item.op === "remove") {
|
||||||
|
await db.relations.unlink(
|
||||||
|
{ type: "notebook", id: item.id },
|
||||||
|
{ id: notebookId, type: "notebook" }
|
||||||
|
);
|
||||||
|
} else if (item.op === "add") {
|
||||||
|
await db.relations.add(
|
||||||
|
{ type: "notebook", id: item.id },
|
||||||
|
{ id: notebookId, type: "notebook" }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await notebookStore.refresh();
|
||||||
|
_onClose(true);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
negativeButton={{
|
||||||
|
text: strings.cancel(),
|
||||||
|
onClick: () => _onClose(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
id="subnotebooks"
|
||||||
|
variant="columnFill"
|
||||||
|
sx={{
|
||||||
|
height: "80vh",
|
||||||
|
px: 4
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Field
|
||||||
|
autoFocus
|
||||||
|
sx={{ m: 0, mb: 2 }}
|
||||||
|
styles={{
|
||||||
|
input: { p: "7.5px" }
|
||||||
|
}}
|
||||||
|
placeholder={strings.searchNotebooks()}
|
||||||
|
onChange={async (e) => {
|
||||||
|
const query = e.target.value.trim();
|
||||||
|
const ids = await (query
|
||||||
|
? db.lookup.notebooks(query).ids()
|
||||||
|
: db.notebooks.roots.ids(
|
||||||
|
db.settings.getGroupOptions("notebooks")
|
||||||
|
));
|
||||||
|
setNotebooks(ids);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{notebooks.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<VirtualizedTree
|
||||||
|
rootId={"root"}
|
||||||
|
itemHeight={30}
|
||||||
|
treeRef={treeRef}
|
||||||
|
getChildNodes={async (id, depth) => {
|
||||||
|
const parentNotebookId = await getParentNotebookId(
|
||||||
|
notebookId
|
||||||
|
);
|
||||||
|
const nodes: TreeNode<Notebook>[] = [];
|
||||||
|
if (id === "root") {
|
||||||
|
for (const id of notebooks) {
|
||||||
|
if (id === notebookId) continue;
|
||||||
|
const notebook = (await db.notebooks.notebook(id))!;
|
||||||
|
const childrenCount = await db.relations
|
||||||
|
.from(notebook, "notebook")
|
||||||
|
.count();
|
||||||
|
const isParent = parentNotebookId === notebook.id;
|
||||||
|
nodes.push({
|
||||||
|
data: notebook,
|
||||||
|
depth: depth + 1,
|
||||||
|
id,
|
||||||
|
parentId: "root",
|
||||||
|
hasChildren: isParent
|
||||||
|
? childrenCount !== 1
|
||||||
|
: childrenCount > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return nodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
const subNotebooks = await db.relations
|
||||||
|
.from({ type: "notebook", id }, "notebook")
|
||||||
|
.resolve();
|
||||||
|
|
||||||
|
for (const notebook of subNotebooks) {
|
||||||
|
if (notebook.id === notebookId) continue;
|
||||||
|
|
||||||
|
const childrenCount = await db.relations
|
||||||
|
.from(notebook, "notebook")
|
||||||
|
.count();
|
||||||
|
const isParent = parentNotebookId === notebook.id;
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
parentId: id,
|
||||||
|
id: notebook.id,
|
||||||
|
data: notebook,
|
||||||
|
depth: depth + 1,
|
||||||
|
hasChildren: isParent
|
||||||
|
? childrenCount !== 1
|
||||||
|
: childrenCount > 0
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodes;
|
||||||
|
}}
|
||||||
|
renderItem={({ item, expanded, index, collapse, expand }) => (
|
||||||
|
<NotebookItem
|
||||||
|
notebook={item.data}
|
||||||
|
depth={item.depth}
|
||||||
|
isExpandable={item.hasChildren}
|
||||||
|
isExpanded={expanded}
|
||||||
|
toggle={expanded ? collapse : expand}
|
||||||
|
onCreateItem={() => {
|
||||||
|
treeRef.current?.refreshItem(index, item.data, {
|
||||||
|
expand: true
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Flex
|
||||||
|
sx={{
|
||||||
|
my: 2,
|
||||||
|
flexDirection: "column",
|
||||||
|
justifyContent: "center",
|
||||||
|
alignItems: "center"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Text variant="body">{strings.notebooksEmpty()}</Text>
|
||||||
|
<Button
|
||||||
|
data-test-id="add-new-notebook"
|
||||||
|
variant="secondary"
|
||||||
|
sx={{ mt: 2 }}
|
||||||
|
onClick={() =>
|
||||||
|
AddNotebookDialog.show({}).then((res) =>
|
||||||
|
res
|
||||||
|
? db.notebooks.roots
|
||||||
|
.ids(db.settings.getGroupOptions("notebooks"))
|
||||||
|
.then((ids) => setNotebooks(ids))
|
||||||
|
: null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{strings.addNotebook()}
|
||||||
|
</Button>
|
||||||
|
</Flex>
|
||||||
|
)}
|
||||||
|
</Flex>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
async function getParentNotebookId(notebookId: string) {
|
||||||
|
const relation = await db.relations
|
||||||
|
.to(
|
||||||
|
{
|
||||||
|
id: notebookId,
|
||||||
|
type: "notebook"
|
||||||
|
},
|
||||||
|
"notebook"
|
||||||
|
)
|
||||||
|
.get();
|
||||||
|
return relation[0]?.fromId;
|
||||||
|
}
|
||||||
@@ -1,17 +1,11 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"POT-Creation-Date: 2025-04-10 10:29+0500\n"
|
"POT-Creation-Date: 2025-06-05 11:43+0500\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Generator: @lingui/cli\n"
|
"X-Generator: @lingui/cli\n"
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
"Project-Id-Version: \n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"PO-Revision-Date: \n"
|
|
||||||
"Last-Translator: \n"
|
|
||||||
"Language-Team: \n"
|
|
||||||
"Plural-Forms: \n"
|
|
||||||
|
|
||||||
#: src/strings.ts:2410
|
#: src/strings.ts:2410
|
||||||
msgid " \"Notebook > Notes\""
|
msgid " \"Notebook > Notes\""
|
||||||
@@ -5403,6 +5397,10 @@ msgstr "Select"
|
|||||||
msgid "Select a backup file from your device to restore backup"
|
msgid "Select a backup file from your device to restore backup"
|
||||||
msgstr "Select a backup file from your device to restore backup"
|
msgstr "Select a backup file from your device to restore backup"
|
||||||
|
|
||||||
|
#: src/strings.ts:2488
|
||||||
|
msgid "Select a notebook to move this notebook into, or unselect to move it to the root level."
|
||||||
|
msgstr "Select a notebook to move this notebook into, or unselect to move it to the root level."
|
||||||
|
|
||||||
#: src/strings.ts:2097
|
#: src/strings.ts:2097
|
||||||
msgid "Select a theme"
|
msgid "Select a theme"
|
||||||
msgstr "Select a theme"
|
msgstr "Select a theme"
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"POT-Creation-Date: 2025-04-10 10:29+0500\n"
|
"POT-Creation-Date: 2025-06-05 11:43+0500\n"
|
||||||
"MIME-Version: 1.0\n"
|
"MIME-Version: 1.0\n"
|
||||||
"Content-Type: text/plain; charset=utf-8\n"
|
"Content-Type: text/plain; charset=utf-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
"X-Generator: @lingui/cli\n"
|
"X-Generator: @lingui/cli\n"
|
||||||
"Language: pseudo-LOCALE\n"
|
"Language: pseudo-LOCALE\n"
|
||||||
"Project-Id-Version: \n"
|
|
||||||
"Report-Msgid-Bugs-To: \n"
|
|
||||||
"PO-Revision-Date: \n"
|
|
||||||
"Last-Translator: \n"
|
|
||||||
"Language-Team: \n"
|
|
||||||
"Plural-Forms: \n"
|
|
||||||
|
|
||||||
#: src/strings.ts:2410
|
#: src/strings.ts:2410
|
||||||
msgid " \"Notebook > Notes\""
|
msgid " \"Notebook > Notes\""
|
||||||
@@ -5377,6 +5371,10 @@ msgstr ""
|
|||||||
msgid "Select a backup file from your device to restore backup"
|
msgid "Select a backup file from your device to restore backup"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
#: src/strings.ts:2488
|
||||||
|
msgid "Select a notebook to move this notebook into, or unselect to move it to the root level."
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
#: src/strings.ts:2097
|
#: src/strings.ts:2097
|
||||||
msgid "Select a theme"
|
msgid "Select a theme"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|||||||
@@ -2483,5 +2483,7 @@ Use this if changes from other devices are not appearing on this device. This wi
|
|||||||
unsetAsHomepage: () => t`Reset homepage`,
|
unsetAsHomepage: () => t`Reset homepage`,
|
||||||
archive: () => t`Archive`,
|
archive: () => t`Archive`,
|
||||||
yourArchiveIsEmpty: () => t`Your archive is empty`,
|
yourArchiveIsEmpty: () => t`Your archive is empty`,
|
||||||
unarchive: () => t`Unarchive`
|
unarchive: () => t`Unarchive`,
|
||||||
|
moveNotebookDesc: () =>
|
||||||
|
t`Select a notebook to move this notebook into, or unselect to move it to the root level.`
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user