Add tag tree API and frontend tag decryption
- Implement backend tag tree endpoint at /tags/tree with TagTreeNode and TagTreeResponse models - Add frontend tag tree decryption logic and wire it into notes decryption flow - Fetch and decrypt tag tree in tags.tsx; integrate with tag store - Add UI toggle for folders vs tags and update Sidebar and Header
This commit is contained in:
parent
c01a1fc908
commit
0abeb90cb0
11 changed files with 165 additions and 103 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@ node_modules
|
||||||
frontend/src/assets/fontawesome/svg/*
|
frontend/src/assets/fontawesome/svg/*
|
||||||
frontend/src/assets/fontawesome/svg/0.svg
|
frontend/src/assets/fontawesome/svg/0.svg
|
||||||
*.db
|
*.db
|
||||||
|
.zed/settings.json
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -1,7 +1,20 @@
|
||||||
|
from tkinter.constants import TOP
|
||||||
|
|
||||||
from app.auth import require_auth
|
from app.auth import require_auth
|
||||||
from app.database import get_session
|
from app.database import get_session
|
||||||
from app.models import Note, NoteCreate, NoteTag, NoteUpdate, Tag, TagCreate, User
|
from app.models import (
|
||||||
|
Note,
|
||||||
|
NoteCreate,
|
||||||
|
NoteTag,
|
||||||
|
NoteUpdate,
|
||||||
|
Tag,
|
||||||
|
TagCreate,
|
||||||
|
TagTreeNode,
|
||||||
|
TagTreeResponse,
|
||||||
|
User,
|
||||||
|
)
|
||||||
from fastapi import APIRouter, Depends, HTTPException
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
from sqlalchemy.orm import selectinload
|
||||||
from sqlmodel import Session, select
|
from sqlmodel import Session, select
|
||||||
|
|
||||||
router = APIRouter(prefix="/tags", tags=["tags"])
|
router = APIRouter(prefix="/tags", tags=["tags"])
|
||||||
|
|
@ -27,6 +40,27 @@ def create_tag(
|
||||||
return db_tag
|
return db_tag
|
||||||
|
|
||||||
|
|
||||||
|
def build_tag_tree_node(tag: Tag) -> TagTreeNode:
|
||||||
|
return TagTreeNode(
|
||||||
|
id= tag.id,
|
||||||
|
name = tag.name,
|
||||||
|
children = [build_tag_tree_node(child) for child in tag.children]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/tree")
|
||||||
|
def get_tag_tree(session: Session = Depends(get_session)):
|
||||||
|
top_level_tags = session.exec(
|
||||||
|
select(Tag)
|
||||||
|
.options(selectinload(Tag.children))
|
||||||
|
.where(Tag.parent_id == None)
|
||||||
|
).all()
|
||||||
|
|
||||||
|
tree = [build_tag_tree_node(tag) for tag in top_level_tags]
|
||||||
|
return TagTreeResponse(tags=tree)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/note/{note_id}/tag/{tag_id}")
|
@router.post("/note/{note_id}/tag/{tag_id}")
|
||||||
def add_tag_to_note(
|
def add_tag_to_note(
|
||||||
note_id: int,
|
note_id: int,
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { FolderTreeResponse, FolderTreeNode } from "./folders";
|
import { FolderTreeResponse, FolderTreeNode } from "./folders";
|
||||||
|
import { Tag } from "./tags";
|
||||||
|
|
||||||
export async function deriveKey(password: string, salt: string) {
|
export async function deriveKey(password: string, salt: string) {
|
||||||
const enc = new TextEncoder();
|
const enc = new TextEncoder();
|
||||||
|
|
@ -147,3 +148,25 @@ export async function decryptFolderTree(
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const decryptTagTree = async (
|
||||||
|
tags: Tag[],
|
||||||
|
key: CryptoKey,
|
||||||
|
parentPath = "",
|
||||||
|
): Promise<Tag[]> => {
|
||||||
|
return Promise.all(
|
||||||
|
tags.map(async (tag) => {
|
||||||
|
const decryptedName = await decryptString(tag.name, key);
|
||||||
|
const currentPath = parentPath
|
||||||
|
? `${parentPath} › ${decryptedName}`
|
||||||
|
: decryptedName;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...tag,
|
||||||
|
name: decryptedName,
|
||||||
|
parent_path: parentPath,
|
||||||
|
children: await decryptTagTree(tag.children, key, currentPath),
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,12 @@ const fetchNotes = async () => {
|
||||||
...note,
|
...note,
|
||||||
title: await decryptString(note.title, encryptionKey),
|
title: await decryptString(note.title, encryptionKey),
|
||||||
content: await decryptString(note.content, encryptionKey),
|
content: await decryptString(note.content, encryptionKey),
|
||||||
tags: note.tags.map(async (tag) => ({
|
tags: await Promise.all(
|
||||||
...tag,
|
note.tags.map(async (tag) => ({
|
||||||
name: await decryptString(tag.name, encryptionKey),
|
...tag,
|
||||||
})),
|
name: await decryptString(tag.name, encryptionKey),
|
||||||
|
})),
|
||||||
|
),
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
return decryptedNotes;
|
return decryptedNotes;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { encryptString, decryptString } from "./encryption";
|
import { encryptString, decryptTagTree } from "./encryption";
|
||||||
import { useAuthStore } from "../stores/authStore";
|
import { useAuthStore } from "../stores/authStore";
|
||||||
axios.defaults.withCredentials = true;
|
axios.defaults.withCredentials = true;
|
||||||
const API_URL = (import.meta as any).env.PROD
|
const API_URL = (import.meta as any).env.PROD
|
||||||
|
|
@ -20,42 +20,13 @@ export interface TagCreate {
|
||||||
parent_id?: number;
|
parent_id?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
const buildTagTree = (
|
|
||||||
tags: Tag[],
|
|
||||||
parent_id: string | number | null = null,
|
|
||||||
parentPath = "",
|
|
||||||
): Tag[] => {
|
|
||||||
const result: Tag[] = [];
|
|
||||||
for (const tag of tags) {
|
|
||||||
if (tag.parent_id == parent_id) {
|
|
||||||
tag.parent_path = parentPath;
|
|
||||||
|
|
||||||
const currentPath = parentPath ? `${parentPath} › ${tag.name}` : tag.name;
|
|
||||||
|
|
||||||
tag.children = buildTagTree(tags, tag.id, currentPath);
|
|
||||||
result.push(tag);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const fetchTags = async () => {
|
const fetchTags = async () => {
|
||||||
const encryptionKey = useAuthStore.getState().encryptionKey;
|
const encryptionKey = useAuthStore.getState().encryptionKey;
|
||||||
if (!encryptionKey) throw new Error("Not authenticated");
|
if (!encryptionKey) throw new Error("Not authenticated");
|
||||||
|
|
||||||
const { data } = await axios.get(`${API_URL}/tags/`);
|
const { data } = await axios.get(`${API_URL}/tags/tree`);
|
||||||
|
const tags = decryptTagTree(data.tags, encryptionKey);
|
||||||
const decryptedTags = await Promise.all(
|
console.log(await tags);
|
||||||
data.map(async (tag: Tag) => ({
|
|
||||||
...tag,
|
|
||||||
name: await decryptString(tag.name, encryptionKey),
|
|
||||||
})),
|
|
||||||
);
|
|
||||||
|
|
||||||
const tags = buildTagTree(decryptedTags);
|
|
||||||
|
|
||||||
console.log(tags);
|
|
||||||
|
|
||||||
return tags;
|
return tags;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -105,20 +105,19 @@ function Home() {
|
||||||
{showModal && <Modal />}
|
{showModal && <Modal />}
|
||||||
|
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
|
<button onClick={getTags}>create</button>
|
||||||
{/*<div className="flex flex-col">
|
{/*<div className="flex flex-col">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={tagName}
|
value={tagName}
|
||||||
onChange={(e) => setTagName(e.target.value)}
|
onChange={(e) => setTagName(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<button onClick={createTag}>create</button>
|
|
||||||
{tags.map((tag) => (
|
{tags.map((tag) => (
|
||||||
<button onClick={() => deleteTag(tag.id)} key={tag.id}>
|
<button onClick={() => deleteTag(tag.id)} key={tag.id}>
|
||||||
{tag.name}
|
{tag.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>*/}
|
</div>*/}
|
||||||
<button onClick={() => getTags()}>Click</button>
|
|
||||||
|
|
||||||
{/* Main editor area */}
|
{/* Main editor area */}
|
||||||
<div className="flex flex-col w-full h-screen overflow-hidden">
|
<div className="flex flex-col w-full h-screen overflow-hidden">
|
||||||
|
|
@ -178,16 +177,16 @@ const Modal = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const TagSelector = () => {
|
export const TagSelector = () => {
|
||||||
const { tagTree } = useTagStore();
|
const { tagTree } = useTagStore();
|
||||||
const [value, setValue] = useState("");
|
const [value, setValue] = useState("");
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<input
|
{/*<input
|
||||||
type="text"
|
type="text"
|
||||||
value={value}
|
value={value}
|
||||||
onChange={(e) => setValue(e.target.value)}
|
onChange={(e) => setValue(e.target.value)}
|
||||||
/>
|
/>*/}
|
||||||
{tagTree && tagTree.map((tag) => <TagTree tag={tag} />)}
|
{tagTree && tagTree.map((tag) => <TagTree tag={tag} />)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,8 @@ import React, { useState, useRef, useEffect, SetStateAction } from "react";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import FolderIcon from "@/assets/fontawesome/svg/folder.svg?react";
|
import FolderIcon from "@/assets/fontawesome/svg/folder.svg?react";
|
||||||
|
// @ts-ignore
|
||||||
|
import TagsIcon from "@/assets/fontawesome/svg/tags.svg?react";
|
||||||
import { DraggableNote } from "./subcomponents/DraggableNote";
|
import { DraggableNote } from "./subcomponents/DraggableNote";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
|
@ -19,6 +21,7 @@ import { SidebarHeader } from "./subcomponents/SideBarHeader.tsx";
|
||||||
import { useAuthStore } from "@/stores/authStore.ts";
|
import { useAuthStore } from "@/stores/authStore.ts";
|
||||||
import { useNoteStore } from "@/stores/notesStore.ts";
|
import { useNoteStore } from "@/stores/notesStore.ts";
|
||||||
import { useUIStore } from "@/stores/uiStore.ts";
|
import { useUIStore } from "@/stores/uiStore.ts";
|
||||||
|
import { TagSelector } from "../../Home.tsx";
|
||||||
|
|
||||||
export const Sidebar = () => {
|
export const Sidebar = () => {
|
||||||
const [newFolder, setNewFolder] = useState(false);
|
const [newFolder, setNewFolder] = useState(false);
|
||||||
|
|
@ -39,7 +42,8 @@ export const Sidebar = () => {
|
||||||
|
|
||||||
const { encryptionKey } = useAuthStore();
|
const { encryptionKey } = useAuthStore();
|
||||||
|
|
||||||
const { setSideBarResize, sideBarResize } = useUIStore();
|
const { setSideBarResize, sideBarResize, sideBarView, setSideBarView } =
|
||||||
|
useUIStore();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (newFolder && newFolderRef.current) {
|
if (newFolder && newFolderRef.current) {
|
||||||
newFolderRef.current.focus();
|
newFolderRef.current.focus();
|
||||||
|
|
@ -169,65 +173,73 @@ export const Sidebar = () => {
|
||||||
style={{ width: `${sideBarResize}px` }}
|
style={{ width: `${sideBarResize}px` }}
|
||||||
>
|
>
|
||||||
<SidebarHeader setNewFolder={setNewFolder} />
|
<SidebarHeader setNewFolder={setNewFolder} />
|
||||||
<div
|
{sideBarView == "folders" ? (
|
||||||
className="bg-ctp-mantle min-h-full border-r border-ctp-surface2 w-full p-4 overflow-y-auto sm:block hidden flex-col gap-3"
|
<>
|
||||||
onDragOver={(e) => e.preventDefault()}
|
<div
|
||||||
onTouchMove={(e) => e.preventDefault()}
|
className="bg-ctp-mantle min-h-full border-r border-ctp-surface2 w-full p-4 overflow-y-auto sm:block hidden flex-col gap-3"
|
||||||
>
|
onDragOver={(e) => e.preventDefault()}
|
||||||
{/* New folder input */}
|
onTouchMove={(e) => e.preventDefault()}
|
||||||
{newFolder && (
|
>
|
||||||
<div className="mb-2">
|
{/* New folder input */}
|
||||||
<input
|
{newFolder && (
|
||||||
onBlur={() => setNewFolder(false)}
|
<div className="mb-2">
|
||||||
onChange={(e) => setNewFolderText(e.target.value)}
|
<input
|
||||||
value={newFolderText}
|
onBlur={() => setNewFolder(false)}
|
||||||
type="text"
|
onChange={(e) => setNewFolderText(e.target.value)}
|
||||||
placeholder="Folder name..."
|
value={newFolderText}
|
||||||
className="standard-input"
|
type="text"
|
||||||
ref={newFolderRef}
|
placeholder="Folder name..."
|
||||||
onKeyDown={(e) => {
|
className="standard-input"
|
||||||
if (e.key === "Enter") {
|
ref={newFolderRef}
|
||||||
handleCreateFolder();
|
onKeyDown={(e) => {
|
||||||
}
|
if (e.key === "Enter") {
|
||||||
if (e.key === "Escape") {
|
handleCreateFolder();
|
||||||
setNewFolder(false);
|
}
|
||||||
}
|
if (e.key === "Escape") {
|
||||||
}}
|
setNewFolder(false);
|
||||||
/>
|
}
|
||||||
</div>
|
}}
|
||||||
)}
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Folder tree */}
|
{/* Folder tree */}
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
{folderTree?.folders.map((folder) => (
|
{folderTree?.folders.map((folder) => (
|
||||||
<FolderTree key={folder.id} folder={folder} depth={0} />
|
<FolderTree key={folder.id} folder={folder} depth={0} />
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Orphaned notes */}
|
|
||||||
{folderTree?.orphaned_notes &&
|
|
||||||
folderTree.orphaned_notes.length > 0 && (
|
|
||||||
<div className="mt-4 flex flex-col gap-1">
|
|
||||||
{folderTree.orphaned_notes.map((note) => (
|
|
||||||
<DraggableNote key={note.id} note={note} />
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<DragOverlay>
|
{/* Orphaned notes */}
|
||||||
{activeItem?.type === "note" && (
|
{folderTree?.orphaned_notes &&
|
||||||
<div className="bg-ctp-surface0 rounded-md px-2 py-1 shadow-lg border border-ctp-mauve">
|
folderTree.orphaned_notes.length > 0 && (
|
||||||
{activeItem.data.title}
|
<div className="mt-4 flex flex-col gap-1">
|
||||||
|
{folderTree.orphaned_notes.map((note) => (
|
||||||
|
<DraggableNote key={note.id} note={note} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
{activeItem?.type === "folder" && (
|
<DragOverlay>
|
||||||
<div className="bg-ctp-surface0 rounded-md px-1 py-0.5 shadow-lg flex items-center gap-1 text-sm">
|
{activeItem?.type === "note" && (
|
||||||
<FolderIcon className="w-3 h-3 fill-ctp-mauve mr-1" />
|
<div className="bg-ctp-surface0 rounded-md px-2 py-1 shadow-lg border border-ctp-mauve">
|
||||||
{activeItem.data.name}
|
{activeItem.data.title}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DragOverlay>
|
{activeItem?.type === "folder" && (
|
||||||
|
<div className="bg-ctp-surface0 rounded-md px-1 py-0.5 shadow-lg flex items-center gap-1 text-sm">
|
||||||
|
<FolderIcon className="w-3 h-3 fill-ctp-mauve mr-1" />
|
||||||
|
{activeItem.data.name}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</DragOverlay>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="bg-ctp-mantle min-h-full border-r border-ctp-surface2 w-full p-4 overflow-y-auto sm:block hidden flex-col gap-3">
|
||||||
|
<TagSelector />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DndContext>
|
</DndContext>
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,11 @@ import { SetStateAction } from "react";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import FolderPlusIcon from "@assets/fontawesome/svg/folder-plus.svg?react";
|
import FolderPlusIcon from "@assets/fontawesome/svg/folder-plus.svg?react";
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
|
import TagsIcon from "@assets/fontawesome/svg/tags.svg?react";
|
||||||
|
// @ts-ignore
|
||||||
import FileCirclePlusIcon from "@assets/fontawesome/svg/file-circle-plus.svg?react";
|
import FileCirclePlusIcon from "@assets/fontawesome/svg/file-circle-plus.svg?react";
|
||||||
import { useNoteStore } from "@/stores/notesStore";
|
import { useNoteStore } from "@/stores/notesStore";
|
||||||
|
import { useUIStore } from "@/stores/uiStore";
|
||||||
|
|
||||||
export const SidebarHeader = ({
|
export const SidebarHeader = ({
|
||||||
setNewFolder,
|
setNewFolder,
|
||||||
|
|
@ -11,6 +14,7 @@ export const SidebarHeader = ({
|
||||||
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
|
setNewFolder: React.Dispatch<SetStateAction<boolean>>;
|
||||||
}) => {
|
}) => {
|
||||||
const { createNote, selectedFolder } = useNoteStore();
|
const { createNote, selectedFolder } = useNoteStore();
|
||||||
|
const { setSideBarView, sideBarView } = useUIStore();
|
||||||
const handleCreate = async () => {
|
const handleCreate = async () => {
|
||||||
await createNote({
|
await createNote({
|
||||||
title: "Untitled",
|
title: "Untitled",
|
||||||
|
|
@ -22,17 +26,26 @@ export const SidebarHeader = ({
|
||||||
<div className="flex items-center justify-center w-full gap-2 bg-ctp-mantle border-b border-ctp-surface0 p-1">
|
<div className="flex items-center justify-center w-full gap-2 bg-ctp-mantle border-b border-ctp-surface0 p-1">
|
||||||
<button
|
<button
|
||||||
onClick={() => setNewFolder(true)}
|
onClick={() => setNewFolder(true)}
|
||||||
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-2"
|
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1"
|
||||||
title="New folder"
|
title="New folder"
|
||||||
>
|
>
|
||||||
<FolderPlusIcon className="w-4 h-4 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
|
<FolderPlusIcon className="w-5 h-5 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() =>
|
||||||
|
setSideBarView(sideBarView == "tags" ? "folders" : "tags")
|
||||||
|
}
|
||||||
|
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1"
|
||||||
|
title="Tags"
|
||||||
|
>
|
||||||
|
<TagsIcon className="w-5 h-5 group-hover:fill-ctp-base transition-colors fill-ctp-mauve" />
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleCreate}
|
onClick={handleCreate}
|
||||||
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-2 fill-ctp-mauve hover:fill-ctp-base"
|
className="hover:bg-ctp-mauve group transition-colors rounded-sm p-1 m-1 fill-ctp-mauve hover:fill-ctp-base"
|
||||||
title="New note"
|
title="New note"
|
||||||
>
|
>
|
||||||
<FileCirclePlusIcon className="w-4 h-4 text-ctp-mauve group-hover:text-ctp-base transition-colors" />
|
<FileCirclePlusIcon className="w-5 h-5 text-ctp-mauve group-hover:text-ctp-base transition-colors" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,7 @@ export const useNoteStore = create<NoteState>()(
|
||||||
(set, get) => ({
|
(set, get) => ({
|
||||||
loadFolderTree: async () => {
|
loadFolderTree: async () => {
|
||||||
const data = await folderApi.tree();
|
const data = await folderApi.tree();
|
||||||
console.log(data);
|
// console.log(data);
|
||||||
set({ folderTree: data });
|
set({ folderTree: data });
|
||||||
},
|
},
|
||||||
folderTree: null,
|
folderTree: null,
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,9 @@ interface UIState {
|
||||||
|
|
||||||
sideBarResize: number;
|
sideBarResize: number;
|
||||||
setSideBarResize: (size: number) => void;
|
setSideBarResize: (size: number) => void;
|
||||||
|
|
||||||
|
sideBarView: string;
|
||||||
|
setSideBarView: (view: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUIStore = create<UIState>()(
|
export const useUIStore = create<UIState>()(
|
||||||
|
|
@ -27,6 +30,10 @@ export const useUIStore = create<UIState>()(
|
||||||
setSideBarResize: (size) => {
|
setSideBarResize: (size) => {
|
||||||
set({ sideBarResize: size });
|
set({ sideBarResize: size });
|
||||||
},
|
},
|
||||||
|
sideBarView: "folders",
|
||||||
|
setSideBarView: (view) => {
|
||||||
|
set({ sideBarView: view });
|
||||||
|
},
|
||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: "ui-store",
|
name: "ui-store",
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue