mobile: refactor

This commit is contained in:
Ammar Ahmed
2025-02-20 14:42:01 +05:00
committed by Abdullah Atta
parent d33f3c4769
commit 905080e743
7 changed files with 336 additions and 491 deletions

View File

@@ -64,7 +64,8 @@ export const SectionHeader = React.memo<
<View
style={{
width: "100%",
paddingHorizontal: DefaultAppStyles.GAP
paddingHorizontal: DefaultAppStyles.GAP,
marginBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<View

View File

@@ -17,44 +17,41 @@ 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 { Note } from "@notesnook/core";
import { Note, Notebook } from "@notesnook/core";
import { strings } from "@notesnook/intl";
import { useThemeColors } from "@notesnook/theme";
import React, {
RefObject,
useCallback,
useEffect,
useRef,
useState
} from "react";
import {
ActivityIndicator,
Keyboard,
TouchableOpacity,
View
} from "react-native";
import { ActionSheetRef } from "react-native-actions-sheet";
import { FlashList } from "react-native-actions-sheet/dist/src/views/FlashList";
import React, { RefObject, useCallback, useEffect, useRef } from "react";
import { ActivityIndicator, View } from "react-native";
import { ActionSheetRef, FlatList } from "react-native-actions-sheet";
import { db } from "../../../common/database";
import { presentSheet } from "../../../services/event-manager";
import Navigation from "../../../services/navigation";
import {
createNotebookTreeStores,
TreeItem
} from "../../../stores/create-notebook-tree-stores";
import { ItemSelection } from "../../../stores/item-selection-store";
import { useNotebooks } from "../../../stores/use-notebook-store";
import { useRelationStore } from "../../../stores/use-relation-store";
import { useSelectionStore } from "../../../stores/use-selection-store";
import { useSettingStore } from "../../../stores/use-setting-store";
import { updateNotebook } from "../../../utils/notebooks";
import { AppFontSize } from "../../../utils/size";
import { DefaultAppStyles } from "../../../utils/styles";
import { Dialog } from "../../dialog";
import DialogHeader from "../../dialog/dialog-header";
import SheetProvider from "../../sheet-provider";
import { NotebookItem } from "../../side-menu/notebook-item";
import { Button } from "../../ui/button";
import { IconButton } from "../../ui/icon-button";
import Input from "../../ui/input";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import { NotebookItem } from "./notebook-item";
import { useNotebookItemSelectionStore } from "./store";
const {
useNotebookExpandedStore,
useNotebookSelectionStore,
useNotebookTreeStore
} = createNotebookTreeStores(true, true, "link-notebooks-expanded-store");
async function updateInitialSelectionState(items: string[]) {
const relations = await db.relations
@@ -86,7 +83,7 @@ async function updateInitialSelectionState(items: string[]) {
initialSelectionState[id] = "intermediate";
}
}
useNotebookItemSelectionStore.setState({
useNotebookSelectionStore.setState({
initialState: initialSelectionState,
selection: { ...initialSelectionState },
multiSelect: relations.length > 1
@@ -101,31 +98,58 @@ const MoveNoteSheet = ({
actionSheetRef: RefObject<ActionSheetRef>;
}) => {
const { colors } = useThemeColors();
const [rootNotebooks, loading] = useNotebooks();
const [notebooks, loading] = useNotebooks();
const tree = useNotebookTreeStore((state) => state.tree);
const searchQuery = useRef("");
const searchTimer = useRef<NodeJS.Timeout>();
const [notebooks, setNotebooks] = useState(rootNotebooks);
const [filteredNotebooks, setFilteredNotebooks] = React.useState(notebooks);
const loadRootNotebooks = React.useCallback(async () => {
if (!filteredNotebooks) return;
const _notebooks: Notebook[] = [];
for (let i = 0; i < filteredNotebooks.placeholders.length; i++) {
_notebooks[i] = (await filteredNotebooks?.item(i))?.item as Notebook;
}
const items = await useNotebookTreeStore
.getState()
.addNotebooks("root", _notebooks, 0);
useNotebookTreeStore.getState().setTree(items);
}, [filteredNotebooks]);
const updateNotebooks = React.useCallback(() => {
if (searchQuery.current) {
db.lookup
.notebooks(searchQuery.current)
.sorted()
.then((filtered) => {
setFilteredNotebooks(filtered);
});
} else {
setFilteredNotebooks(notebooks);
}
}, [notebooks]);
useEffect(() => {
updateNotebooks();
}, [updateNotebooks]);
useEffect(() => {
(async () => {
if (!loading) {
loadRootNotebooks();
}
})();
}, [loadRootNotebooks, loading]);
const dimensions = useSettingStore((state) => state.dimensions);
const selectedItemsList = useSelectionStore(
(state) => state.selectedItemsList
);
const multiSelect = useNotebookItemSelectionStore(
(state) => state.multiSelect
);
useEffect(() => {
if (!loading) {
setNotebooks(rootNotebooks);
}
}, [loading, rootNotebooks]);
const multiSelect = useNotebookSelectionStore((state) => state.multiSelect);
useEffect(() => {
const items = note ? [note.id] : selectedItemsList;
updateInitialSelectionState(items);
return () => {
useNotebookItemSelectionStore.setState({
useNotebookSelectionStore.setState({
initialState: {},
selection: {},
multiSelect: false,
@@ -137,7 +161,7 @@ const MoveNoteSheet = ({
const onSave = async () => {
const noteIds = note ? [note.id] : selectedItemsList;
const changedNotebooks = useNotebookItemSelectionStore.getState().selection;
const changedNotebooks = useNotebookSelectionStore.getState().selection;
for (const id in changedNotebooks) {
const item = await db.notebooks.notebook(id);
@@ -161,43 +185,39 @@ const MoveNoteSheet = ({
};
const hasSelected = () => {
const selection = useNotebookItemSelectionStore.getState().selection;
const selection = useNotebookSelectionStore.getState().selection;
return Object.keys(selection).some((key) => selection[key] === "selected");
};
const renderNotebook = useCallback(
({ index }: { item: boolean; index: number }) => (
<NotebookItem items={notebooks} id={index} index={index} />
({ item, index }: { item: TreeItem; index: number }) => (
<NotebookItemWrapper item={item} index={index} />
),
[notebooks]
[]
);
return (
<>
<Dialog context="move_note" />
<SheetProvider context="link-notebooks" />
<View>
<TouchableOpacity
style={{
width: "100%",
height: "100%",
position: "absolute"
}}
onPress={() => {
Keyboard.dismiss();
}}
/>
<View
style={{
gap: DefaultAppStyles.GAP_VERTICAL
}}
>
<View
style={{
paddingHorizontal: 12,
paddingHorizontal: DefaultAppStyles.GAP,
justifyContent: "space-between",
flexDirection: "row",
alignItems: "flex-start"
alignItems: "center",
borderBottomWidth: 1,
borderBottomColor: colors.primary.border,
paddingBottom: DefaultAppStyles.GAP_VERTICAL
}}
>
<DialogHeader
style={{
minHeight: 10,
flexShrink: 1
}}
title={strings.selectNotebooks()}
@@ -211,7 +231,7 @@ const MoveNoteSheet = ({
<View
style={{
flexDirection: "row",
columnGap: 10
columnGap: DefaultAppStyles.GAP_SMALL
}}
>
{hasSelected() ? (
@@ -232,112 +252,199 @@ const MoveNoteSheet = ({
<Button
height={40}
style={{
borderRadius: 100,
paddingHorizontal: 24,
alignSelf: "flex-start"
}}
title={strings.save()}
type={"accent"}
type="accent"
onPress={onSave}
/>
</View>
</View>
<View
<FlatList
data={tree}
windowSize={3}
style={{
paddingHorizontal: 0,
maxHeight: dimensions.height * 0.85,
height: dimensions.height * 0.85,
paddingTop: 6
width: "100%"
}}
>
<FlashList
data={notebooks?.placeholders}
style={{
width: "100%"
}}
keyboardShouldPersistTaps="handled"
ListHeaderComponent={
<View
style={{
paddingHorizontal: 12,
width: "100%",
paddingTop: 12
keyboardShouldPersistTaps="handled"
ListHeaderComponent={
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP,
width: "100%",
paddingTop: DefaultAppStyles.GAP_VERTICAL
}}
>
<Input
placeholder={strings.searchNotebooks()}
button={{
icon: "plus",
onPress: () => {
AddNotebookSheet.present(
undefined,
undefined,
"link-notebooks",
undefined,
false,
searchQuery.current
);
},
color: colors.primary.icon
}}
>
<Input
placeholder={strings.searchNotebooks()}
button={{
icon: "plus",
onPress: () => {
AddNotebookSheet.present(
undefined,
undefined,
"link-notebooks",
undefined,
false,
searchQuery.current
);
},
color: colors.primary.icon
}}
onChangeText={(value) => {
searchQuery.current = value;
if (!searchQuery.current) {
setNotebooks(rootNotebooks);
return;
}
searchTimer.current = setTimeout(() => {
db.lookup
.notebooks(searchQuery.current)
.sorted()
.then((result) => {
if (searchQuery.current === value) {
setNotebooks(result);
}
});
}, 300);
}}
onChangeText={(value) => {
searchQuery.current = value;
searchTimer.current = setTimeout(() => {
updateNotebooks();
}, 300);
}}
/>
</View>
}
renderItem={renderNotebook}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
{loading ? (
<ActivityIndicator
size={AppFontSize.lg}
color={colors.primary.accent}
/>
</View>
}
estimatedItemSize={50}
renderItem={renderNotebook}
ListEmptyComponent={
<View
style={{
flex: 1,
justifyContent: "center",
alignItems: "center",
height: 200
}}
>
{loading ? (
<ActivityIndicator
size={AppFontSize.lg}
color={colors.primary.accent}
/>
) : (
<Paragraph color={colors.primary.icon}>
{strings.emptyPlaceholders("notebook")}
</Paragraph>
)}
</View>
}
ListFooterComponent={<View style={{ height: 50 }} />}
/>
</View>
) : (
<Paragraph color={colors.primary.icon}>
{strings.emptyPlaceholders("notebook")}
</Paragraph>
)}
</View>
}
/>
</View>
</>
);
};
const NotebookItemWrapper = React.memo(
({
item,
index,
onPress
}: {
item: TreeItem;
index: number;
onPress?: () => void;
}) => {
const expanded = useNotebookExpandedStore(
(state) => state.expanded[item.notebook.id]
);
const selectionEnabled = useNotebookSelectionStore(
(state) => state.enabled
);
const selected = useNotebookSelectionStore(
(state) => state.selection[item.notebook.id] === "selected"
);
const onItemUpdate = React.useCallback(async () => {
const notebook = await db.notebooks.notebook(item.notebook.id);
if (notebook) {
useNotebookTreeStore.getState().updateItem(item.notebook.id, notebook);
if (expanded) {
useNotebookTreeStore
.getState()
.setTree(
await useNotebookTreeStore
.getState()
.fetchAndAdd(item.notebook.id, item.depth + 1)
);
}
} else {
useNotebookTreeStore.getState().removeItem(item.notebook.id);
}
}, [expanded, item.depth, item.notebook.id]);
return (
<View
style={{
paddingHorizontal: DefaultAppStyles.GAP
}}
>
<NotebookItem
item={item}
index={index}
expanded={expanded}
onToggleExpanded={async () => {
useNotebookExpandedStore.getState().setExpanded(item.notebook.id);
if (!expanded) {
useNotebookTreeStore
.getState()
.setTree(
await useNotebookTreeStore
.getState()
.fetchAndAdd(item.notebook.id, item.depth + 1)
);
} else {
useNotebookTreeStore.getState().removeChildren(item.notebook.id);
}
}}
onLongPress={() => {
useNotebookSelectionStore.setState({
multiSelect: !useNotebookSelectionStore.getState().multiSelect
});
const state = useNotebookSelectionStore.getState();
useNotebookSelectionStore
.getState()
.markAs(
item.notebook,
!selected
? "selected"
: !state.initialState[item.notebook.id]
? undefined
: "deselected"
);
}}
canDisableSelectionMode={false}
selected={selected}
selectionEnabled={selectionEnabled}
selectionStore={useNotebookSelectionStore}
onItemUpdate={onItemUpdate}
focused={false}
onPress={onPress}
onAddNotebook={() => {
AddNotebookSheet.present(
undefined,
item.notebook,
"link-notebooks",
undefined,
false
);
}}
/>
</View>
);
},
(prev, next) => {
return (
prev.item.notebook.id === next.item.notebook.id &&
prev.item.notebook.dateModified === next.item.notebook.dateModified &&
prev.item.notebook.dateEdited === next.item.notebook.dateEdited &&
prev.item.hasChildren === next.item.hasChildren &&
prev.index === next.index &&
prev.item.parentId === next.item.parentId
);
}
);
NotebookItemWrapper.displayName = "NotebookItemWrapper";
MoveNoteSheet.present = (note?: Note) => {
presentSheet({
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />,
enableGesturesInScrollView: false,
noBottomPadding: true,
keyboardHandlerDisabled: true
component: (ref) => <MoveNoteSheet actionSheetRef={ref} note={note} />
});
};
export default MoveNoteSheet;

View File

@@ -1,263 +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 { Notebook, VirtualizedGrouping } from "@notesnook/core";
import { useThemeColors } from "@notesnook/theme";
import React, { useEffect } from "react";
import { View, useWindowDimensions } from "react-native";
import { notesnook } from "../../../../e2e/test.ids";
import { useTotalNotes } from "../../../hooks/use-db-item";
import { useNotebook } from "../../../hooks/use-notebook";
import useNavigationStore from "../../../stores/use-navigation-store";
import { AppFontSize } from "../../../utils/size";
import { IconButton } from "../../ui/icon-button";
import { Pressable } from "../../ui/pressable";
import Paragraph from "../../ui/typography/paragraph";
import { AddNotebookSheet } from "../add-notebook";
import {
useNotebookExpandedStore,
useNotebookItemSelectionStore
} from "./store";
type NotebookParentProp = {
parent?: NotebookParentProp;
item?: Notebook;
};
export const NotebookItem = ({
id,
currentLevel = 0,
index,
parent,
items
}: {
id: string | number;
currentLevel?: number;
index: number;
parent?: NotebookParentProp;
items?: VirtualizedGrouping<Notebook>;
}) => {
const { nestedNotebooks, notebook: item } = useNotebook(id, items, true);
const expanded = useNotebookExpandedStore((state) =>
item?.id ? state.expanded[item?.id] : false
);
const { totalNotes: totalNotes, getTotalNotes } = useTotalNotes("notebook");
const focusedRouteId = useNavigationStore((state) => state.focusedRouteId);
const { colors } = useThemeColors("sheet");
const selection = useNotebookItemSelectionStore((state) =>
item?.id ? state.selection[item?.id] : undefined
);
const isSelected = selection === "selected";
const isFocused = focusedRouteId === id;
const { fontScale } = useWindowDimensions();
useEffect(() => {
if (item?.id) {
getTotalNotes([item?.id]);
}
}, [getTotalNotes, item?.id]);
const onPress = () => {
if (!item) return;
const state = useNotebookItemSelectionStore.getState();
if (isSelected) {
state.markAs(
item,
!state.initialState[item?.id] ? undefined : "deselected"
);
return;
}
if (!state.multiSelect) {
const keys = Object.keys(state.selection);
const nextState: any = {};
for (const key in keys) {
nextState[key] = !state.initialState[key] ? undefined : "deselected";
}
state.setSelection({
[item.id]: "selected",
...nextState
});
} else {
state.markAs(item, "selected");
}
};
return (
<View
style={{
paddingLeft: currentLevel > 0 && currentLevel < 6 ? 15 : undefined,
width: "100%"
}}
>
<Pressable
type={"transparent"}
onLongPress={() => {
if (!item) return;
useNotebookItemSelectionStore.setState({
multiSelect: true
});
const state = useNotebookItemSelectionStore.getState();
useNotebookItemSelectionStore
.getState()
.markAs(
item,
!isSelected
? "selected"
: !state.initialState[item?.id]
? undefined
: "deselected"
);
}}
testID={`add-to-notebook-item-${currentLevel}-${index}`}
onPress={onPress}
style={{
justifyContent: "space-between",
width: "100%",
alignItems: "center",
flexDirection: "row",
paddingLeft: 12,
paddingRight: 12,
borderRadius: 0,
height: 45
}}
>
<View
style={{
flexDirection: "row",
alignItems: "center"
}}
>
{nestedNotebooks?.placeholders.length ? (
<IconButton
size={AppFontSize.xl}
color={isSelected ? colors.selected.icon : colors.primary.icon}
onPress={() => {
if (!item?.id) return;
useNotebookExpandedStore.getState().setExpanded(item?.id);
}}
top={0}
left={0}
bottom={0}
right={0}
style={{
width: 35,
height: 35
}}
name={expanded ? "chevron-down" : "chevron-right"}
/>
) : null}
<IconButton
size={AppFontSize.xl}
color={
isSelected
? colors.selected.icon
: selection === "deselected"
? colors.error.accent
: colors.primary.icon
}
onPress={onPress}
top={0}
left={0}
bottom={0}
right={0}
style={{
width: 40,
height: 40
}}
name={
selection === "deselected"
? "close-circle-outline"
: isSelected
? "check-circle-outline"
: selection === "intermediate"
? "minus-circle-outline"
: "checkbox-blank-circle-outline"
}
/>
<Paragraph
color={
isFocused ? colors.selected.paragraph : colors.secondary.paragraph
}
size={AppFontSize.sm}
>
{item?.title}
</Paragraph>
</View>
<View
style={{
flexDirection: "row",
columnGap: 10,
alignItems: "center"
}}
>
{item?.id && totalNotes?.(item?.id) ? (
<Paragraph size={AppFontSize.sm} color={colors.secondary.paragraph}>
{totalNotes(item?.id)}
</Paragraph>
) : null}
<IconButton
name="plus"
style={{
width: 40 * fontScale,
height: 40 * fontScale
}}
testID={notesnook.ids.notebook.menu}
onPress={() => {
if (!item) return;
AddNotebookSheet.present(
undefined,
item,
"link-notebooks",
undefined,
false
);
}}
left={0}
right={0}
bottom={0}
top={0}
color={colors.primary.icon}
size={AppFontSize.xl}
/>
</View>
</Pressable>
{!expanded
? null
: nestedNotebooks?.placeholders.map((id, index) => (
<NotebookItem
key={item?.id + "_" + index}
id={index}
index={index}
currentLevel={currentLevel + 1}
items={nestedNotebooks}
parent={{
parent: parent,
item: item
}}
/>
))}
</View>
);
};

View File

@@ -1,39 +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 create from "zustand";
import { createItemSelectionStore } from "../../../stores/item-selection-store";
export const useNotebookExpandedStore = create<{
expanded: {
[id: string]: boolean;
};
setExpanded: (id: string) => void;
}>((set, get) => ({
expanded: {},
setExpanded(id: string) {
set({
expanded: {
...get().expanded,
[id]: !get().expanded[id]
}
});
}
}));
export const useNotebookItemSelectionStore = createItemSelectionStore(true);

View File

@@ -63,9 +63,9 @@ export const MoveNotebookSheet = ({
selectedNotebooks: Notebook[];
close?: () => void;
}) => {
const tree = useNotebookTreeStore((state) => state.tree);
const [notebooks, loading] = useNotebooks();
const { colors } = useThemeColors();
const tree = useNotebookTreeStore((state) => state.tree);
const lastQuery = React.useRef<string>();
const [filteredNotebooks, setFilteredNotebooks] = React.useState(notebooks);
const [moveToTopEnabled, setMoveToTopEnabled] = useState(false);

View File

@@ -48,7 +48,8 @@ export const NotebookItem = ({
onItemUpdate,
onPress,
onLongPress,
onAddNotebook
onAddNotebook,
canDisableSelectionMode
}: {
index: number;
item: TreeItem;
@@ -62,6 +63,7 @@ export const NotebookItem = ({
onPress?: () => void;
onLongPress?: () => void;
onAddNotebook?: () => void;
canDisableSelectionMode?: boolean;
}) => {
const notebook = item.notebook;
const isFocused = focused;
@@ -101,10 +103,34 @@ export const NotebookItem = ({
testID={`notebook-item-${item.depth}-${index}`}
onPress={async () => {
if (selectionEnabled) {
selectionStore
.getState()
.markAs(item.notebook, selected ? "deselected" : "selected");
if (selectionStore.getState().getSelectedItemIds().length === 0) {
const state = selectionStore.getState();
if (selected) {
state.markAs(item.notebook, "deselected");
return;
}
if (!state.multiSelect) {
const keys = Object.keys(state.selection);
const nextState: any = {};
for (const key in keys) {
nextState[key] = !state.initialState[key]
? undefined
: "deselected";
}
state.setSelection({
[item.notebook.id]: "selected",
...nextState
});
} else {
state.markAs(item.notebook, "selected");
}
if (
selectionStore.getState().getSelectedItemIds().length === 0 &&
canDisableSelectionMode
) {
selectionStore.setState({
enabled: false
});
@@ -167,48 +193,59 @@ export const NotebookItem = ({
</Paragraph>
</View>
{selectionEnabled ? (
<View
style={{
width: 25,
height: 25,
justifyContent: "center",
alignItems: "center"
}}
>
<AppIcon
name={selected ? "checkbox-outline" : "checkbox-blank-outline"}
size={AppFontSize.md}
color={selected ? colors.selected.icon : colors.primary.icon}
/>
</View>
) : onAddNotebook ? (
<IconButton
name="plus"
size={AppFontSize.md}
top={0}
left={50}
bottom={0}
right={40}
style={{
width: 32,
height: 32,
borderRadius: defaultBorderRadius
}}
onPress={() => {
onAddNotebook();
}}
/>
) : (
<>
<Paragraph
size={AppFontSize.xxs}
color={colors.secondary.paragraph}
<View
style={{
gap: DefaultAppStyles.GAP_SMALL,
flexDirection: "row",
alignItems: "center",
justifyContent: "center"
}}
>
{selectionEnabled ? (
<View
style={{
width: 25,
height: 25,
justifyContent: "center",
alignItems: "center"
}}
>
{totalNotes?.(notebook?.id) || 0}
</Paragraph>
</>
)}
<AppIcon
name={selected ? "checkbox-outline" : "checkbox-blank-outline"}
size={AppFontSize.md}
color={selected ? colors.selected.icon : colors.primary.icon}
/>
</View>
) : (
<>
<Paragraph
size={AppFontSize.xxs}
color={colors.secondary.paragraph}
>
{totalNotes?.(notebook?.id) || 0}
</Paragraph>
</>
)}
{onAddNotebook ? (
<IconButton
name="plus"
size={AppFontSize.md}
top={0}
left={50}
bottom={0}
right={40}
style={{
width: 32,
height: 32,
borderRadius: defaultBorderRadius
}}
onPress={() => {
onAddNotebook();
}}
/>
) : null}
</View>
</Pressable>
</View>
);

View File

@@ -39,7 +39,9 @@ import {
useSideMenuNotebookSelectionStore,
useSideMenuNotebookTreeStore
} from "./stores";
useSideMenuNotebookSelectionStore.setState({
multiSelect: true
});
export const SideMenuNotebooks = () => {
const tree = useSideMenuNotebookTreeStore((state) => state.tree);
const [notebooks, loading] = useNotebooks();