Просмотр исходного кода

Improve data fetching and update style

https://youtrack.weseek.co.jp/issue/GW-7895
- Create and adjust styling based on page tree style
- Create and separate SWR data fetching to load bookmark folder and children
- Update bookmark folder list route
- Add parentId parameter in list route
- Adjust parameter of findParentFolderByUserId method
- Update BookmarkFolderItems type
- Unify SWR data fetching to load initial data and child
Mudana-Grune 3 лет назад
Родитель
Сommit
e237fe959f

+ 5 - 5
packages/app/src/components/Sidebar/Bookmarks.tsx

@@ -7,7 +7,8 @@ import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { OnDeletedFunction } from '~/interfaces/ui';
-import { useSWRxCurrentUserBookmarkFolders, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useIsGuestUser } from '~/stores/context';
 import { useIsGuestUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
 import { usePageDeleteModal } from '~/stores/modal';
 
 
@@ -19,12 +20,11 @@ const Bookmarks = () : JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: currentUserBookmarksData, mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
   const { data: currentUserBookmarksData, mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
-  const { data: currentUserBookmarkFolder, mutate: mutateCurrentUserBookmarkFolder } = useSWRxCurrentUserBookmarkFolders();
+  const { mutate: mutateInitialBookmarkFolderData } = useSWRxBookamrkFolderAndChild(true);
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
 
 
   const [isRenameFolderShown, setIsRenameFolderShown] = useState<boolean>(false);
   const [isRenameFolderShown, setIsRenameFolderShown] = useState<boolean>(false);
   const [folderName, setFolderName] = useState<string>('');
   const [folderName, setFolderName] = useState<string>('');
-  const [currentParentFolder, setCurrentParentFolder] = useState<string | null>(null);
 
 
   const deleteMenuItemClickHandler = (pageToDelete: IPageToDeleteWithMeta) => {
   const deleteMenuItemClickHandler = (pageToDelete: IPageToDeleteWithMeta) => {
     const pageDeletedHandler : OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
     const pageDeletedHandler : OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
@@ -47,10 +47,10 @@ const Bookmarks = () : JSX.Element => {
   const onPressEnterHandler = async(folderName: string) => {
   const onPressEnterHandler = async(folderName: string) => {
     setFolderName(folderName);
     setFolderName(folderName);
     try {
     try {
-      await apiv3Post('/bookmark-folder', { name: folderName, parent: currentParentFolder });
+      await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
       setIsRenameFolderShown(false);
       setIsRenameFolderShown(false);
       setFolderName('');
       setFolderName('');
-      mutateCurrentUserBookmarkFolder();
+      mutateInitialBookmarkFolderData();
       toastSuccess(t('Create New Bookmark Folder Success'));
       toastSuccess(t('Create New Bookmark Folder Success'));
     }
     }
     catch (err) {
     catch (err) {

+ 22 - 16
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolderItem.tsx

@@ -7,45 +7,50 @@ import { useTranslation } from 'next-i18next';
 import CountBadge from '~/components/Common/CountBadge';
 import CountBadge from '~/components/Common/CountBadge';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import { BookmarkFolderItems } from '~/server/models/bookmark-folder';
 import { BookmarkFolderItems } from '~/server/models/bookmark-folder';
-import { useSWRxChildBookmarkFolders } from '~/stores/bookmark';
+import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 
 
 
 
 type BookmarkFolderItemProps = {
 type BookmarkFolderItemProps = {
   bookmarkFolders: BookmarkFolderItems
   bookmarkFolders: BookmarkFolderItems
   isOpen?: boolean
   isOpen?: boolean
+  updateActiveElement?: (parentId: string | null) => void
+  isActive?: boolean
 }
 }
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
-  const { bookmarkFolders, isOpen: _isOpen = false } = props;
+  const {
+    bookmarkFolders, isOpen: _isOpen = false, updateActiveElement, isActive,
+  } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const hasChildren = bookmarkFolders.childCount > 0;
+  const hasChildren = bookmarkFolders.children.length > 0;
   const [currentParentFolder, setCurrentParentFolder] = useState<string | null>(null);
   const [currentParentFolder, setCurrentParentFolder] = useState<string | null>(null);
-  const [isActive, setIsActive] = useState<boolean>(false);
   const [isOpen, setIsOpen] = useState(_isOpen);
   const [isOpen, setIsOpen] = useState(_isOpen);
-  const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxChildBookmarkFolders(isOpen, currentParentFolder);
+  const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(isOpen, currentParentFolder);
 
 
   useEffect(() => {
   useEffect(() => {
     setCurrentParentFolder(bookmarkFolders.bookmarkFolder._id);
     setCurrentParentFolder(bookmarkFolders.bookmarkFolder._id);
   }, [bookmarkFolders]);
   }, [bookmarkFolders]);
 
 
-  const onClickHandler = useCallback(async() => {
+  const loadChildFolder = useCallback(async() => {
     setCurrentParentFolder(bookmarkFolders.bookmarkFolder._id);
     setCurrentParentFolder(bookmarkFolders.bookmarkFolder._id);
     setIsOpen(!isOpen);
     setIsOpen(!isOpen);
-    setIsActive(!isActive);
+    updateActiveElement?.(!isOpen ? bookmarkFolders.bookmarkFolder._id : null);
     mutateChildBookmarkData();
     mutateChildBookmarkData();
-  }, [bookmarkFolders, isOpen, isActive, mutateChildBookmarkData]);
+  }, [bookmarkFolders, isOpen, updateActiveElement, mutateChildBookmarkData]);
+
 
 
   return (
   return (
-    <div id={`bookmark-folder-item-${bookmarkFolders.bookmarkFolder._id}`} className="grw-pagetree-item-container"
+    <div id={`bookmark-folder-item-${bookmarkFolders.bookmarkFolder._id}`} className="grw-foldertree-item-container"
     >
     >
       <li className={`list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center
       <li className={`list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center
-       ${isActive ? 'grw-pagetree-current-page-item' : ''}`}
+       ${isActive ? 'grw-foldertree-current-folder-item' : ''}` }
+      onClick={loadChildFolder}
       >
       >
         <div className="grw-triangle-container d-flex justify-content-center">
         <div className="grw-triangle-container d-flex justify-content-center">
           {hasChildren && (
           {hasChildren && (
             <button
             <button
               type="button"
               type="button"
-              className={`grw-pagetree-triangle-btn btn ${isOpen ? 'grw-pagetree-open' : ''}`}
-              onClick={onClickHandler}
+              className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
+              onClick={loadChildFolder}
             >
             >
               <div className="d-flex justify-content-center">
               <div className="d-flex justify-content-center">
                 <TriangleIcon />
                 <TriangleIcon />
@@ -59,24 +64,25 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
           </div>
           </div>
         }
         }
         {
         {
-          <div className='grw-pagetree-title-anchor flex-grow-1'>
+          <div className='grw-foldertree-title-anchor flex-grow-1'>
             <p className={'text-truncate m-auto '}>{bookmarkFolders.bookmarkFolder.name}</p>
             <p className={'text-truncate m-auto '}>{bookmarkFolders.bookmarkFolder.name}</p>
           </div>
           </div>
         }
         }
         {hasChildren && (
         {hasChildren && (
-          <div className="grw-pagetree-count-wrapper">
-            <CountBadge count={bookmarkFolders.childCount } />
+          <div className="grw-foldertree-count-wrapper">
+            <CountBadge count={ bookmarkFolders.children.length} />
           </div>
           </div>
         )}
         )}
 
 
       </li>
       </li>
       {
       {
         isOpen && hasChildren && childBookmarkFolderData?.map(children => (
         isOpen && hasChildren && childBookmarkFolderData?.map(children => (
-          <div key={children.bookmarkFolder._id} className="grw-pagetree-item-children">
+          <div key={children.bookmarkFolder._id} className="grw-foldertree-item-children">
             <BookmarkFolderItem
             <BookmarkFolderItem
               key={children.bookmarkFolder._id}
               key={children.bookmarkFolder._id}
               bookmarkFolders={children}
               bookmarkFolders={children}
               isOpen = {false}
               isOpen = {false}
+              isActive = {isActive}
             />
             />
           </div>
           </div>
         ))
         ))

+ 39 - 39
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolderTree.module.scss

@@ -1,9 +1,9 @@
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
 $grw-sidebar-content-header-height: 58px;
 $grw-sidebar-content-header-height: 58px;
 $grw-sidebar-content-footer-height: 50px;
 $grw-sidebar-content-footer-height: 50px;
-$grw-pagetree-item-padding-left: 10px;
+$grw-foldertree-item-padding-left: 10px;
 
 
-.grw-pagetree {
+.grw-foldertree {
   :global {
   :global {
     min-height: calc(100vh - (var.$grw-navbar-height + var.$grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
     min-height: calc(100vh - (var.$grw-navbar-height + var.$grw-navbar-border-width + $grw-sidebar-content-header-height + $grw-sidebar-content-footer-height));
 
 
@@ -28,23 +28,23 @@ $grw-pagetree-item-padding-left: 10px;
         }
         }
       }
       }
 
 
-      .grw-pagetree-triangle-btn {
+      .grw-foldertree-triangle-btn {
         background-color: transparent;
         background-color: transparent;
         transition: all 0.2s ease-out;
         transition: all 0.2s ease-out;
         transform: rotate(0deg);
         transform: rotate(0deg);
 
 
-        &.grw-pagetree-open {
+        &.grw-foldertree-open {
           transform: rotate(90deg);
           transform: rotate(90deg);
         }
         }
       }
       }
 
 
-      .grw-pagetree-title-anchor {
+      .grw-foldertree-title-anchor {
         width: 100%;
         width: 100%;
         overflow: hidden;
         overflow: hidden;
         text-decoration: none;
         text-decoration: none;
       }
       }
 
 
-      .grw-pagetree-count-wrapper {
+      .grw-foldertree-count-wrapper {
         display: inline-block;
         display: inline-block;
 
 
         &:hover {
         &:hover {
@@ -53,7 +53,7 @@ $grw-pagetree-item-padding-left: 10px;
       }
       }
     }
     }
 
 
-    .grw-pagetree-item-container {
+    .grw-foldertree-item-container {
       .grw-triangle-container {
       .grw-triangle-container {
         min-width: 35px;
         min-width: 35px;
         height: 40px;
         height: 40px;
@@ -61,60 +61,60 @@ $grw-pagetree-item-padding-left: 10px;
     }
     }
   }
   }
   &:global{
   &:global{
-    // To realize a hierarchical structure, set multiplied padding-left to each pagetree-item
-    > .grw-pagetree-item-container {
+    // To realize a hierarchical structure, set multiplied padding-left to each foldertree-item
+    > .grw-foldertree-item-container {
       > .list-group-item {
       > .list-group-item {
         padding-left: 0;
         padding-left: 0;
       }
       }
-      > .grw-pagetree-item-children {
-        > .grw-pagetree-item-container {
+      > .grw-foldertree-item-children {
+        > .grw-foldertree-item-container {
           > .list-group-item {
           > .list-group-item {
-            padding-left: $grw-pagetree-item-padding-left;
+            padding-left: $grw-foldertree-item-padding-left;
           }
           }
-          > .grw-pagetree-item-children {
-            > .grw-pagetree-item-container {
+          > .grw-foldertree-item-children {
+            > .grw-foldertree-item-container {
               > .list-group-item {
               > .list-group-item {
-                padding-left: $grw-pagetree-item-padding-left * 2;
+                padding-left: $grw-foldertree-item-padding-left * 2;
               }
               }
-              > .grw-pagetree-item-children {
-                > .grw-pagetree-item-container {
+              > .grw-foldertree-item-children {
+                > .grw-foldertree-item-container {
                   > .list-group-item {
                   > .list-group-item {
-                    padding-left: $grw-pagetree-item-padding-left * 3;
+                    padding-left: $grw-foldertree-item-padding-left * 3;
                   }
                   }
-                  > .grw-pagetree-item-children {
-                    > .grw-pagetree-item-container {
+                  > .grw-foldertree-item-children {
+                    > .grw-foldertree-item-container {
                       > .list-group-item {
                       > .list-group-item {
-                        padding-left: $grw-pagetree-item-padding-left * 4;
+                        padding-left: $grw-foldertree-item-padding-left * 4;
                       }
                       }
-                      > .grw-pagetree-item-children {
-                        > .grw-pagetree-item-container {
+                      > .grw-foldertree-item-children {
+                        > .grw-foldertree-item-container {
                           > .list-group-item {
                           > .list-group-item {
-                            padding-left: $grw-pagetree-item-padding-left * 5;
+                            padding-left: $grw-foldertree-item-padding-left * 5;
                           }
                           }
-                          > .grw-pagetree-item-children {
-                            > .grw-pagetree-item-container {
+                          > .grw-foldertree-item-children {
+                            > .grw-foldertree-item-container {
                               > .list-group-item {
                               > .list-group-item {
-                                padding-left: $grw-pagetree-item-padding-left * 6;
+                                padding-left: $grw-foldertree-item-padding-left * 6;
                               }
                               }
-                              > .grw-pagetree-item-children {
-                                > .grw-pagetree-item-container {
+                              > .grw-foldertree-item-children {
+                                > .grw-foldertree-item-container {
                                   > .list-group-item {
                                   > .list-group-item {
-                                    padding-left: $grw-pagetree-item-padding-left * 7;
+                                    padding-left: $grw-foldertree-item-padding-left * 7;
                                   }
                                   }
-                                  > .grw-pagetree-item-children {
-                                    > .grw-pagetree-item-container {
+                                  > .grw-foldertree-item-children {
+                                    > .grw-foldertree-item-container {
                                       > .list-group-item {
                                       > .list-group-item {
-                                        padding-left: $grw-pagetree-item-padding-left * 8;
+                                        padding-left: $grw-foldertree-item-padding-left * 8;
                                       }
                                       }
-                                      > .grw-pagetree-item-children {
-                                        > .grw-pagetree-item-container {
+                                      > .grw-foldertree-item-children {
+                                        > .grw-foldertree-item-container {
                                           > .list-group-item {
                                           > .list-group-item {
-                                            padding-left: $grw-pagetree-item-padding-left * 9;
+                                            padding-left: $grw-foldertree-item-padding-left * 9;
                                           }
                                           }
-                                          .grw-pagetree-item-children {
-                                            > .grw-pagetree-item-container {
+                                          .grw-foldertree-item-children {
+                                            > .grw-foldertree-item-container {
                                               > .list-group-item {
                                               > .list-group-item {
-                                                padding-left: $grw-pagetree-item-padding-left * 10;
+                                                padding-left: $grw-foldertree-item-padding-left * 10;
                                               }
                                               }
                                             }
                                             }
                                           }
                                           }

+ 10 - 4
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolderTree.tsx

@@ -1,8 +1,8 @@
-import React from 'react';
+import React, { useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { useSWRxCurrentUserBookmarkFolders } from '~/stores/bookmark';
+import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 
 
 import BookmarkFolderItem from './BookmarkFolderItem';
 import BookmarkFolderItem from './BookmarkFolderItem';
 
 
@@ -11,19 +11,25 @@ import styles from './BookmarkFolderTree.module.scss';
 
 
 const BookmarkFolderTree = (): JSX.Element => {
 const BookmarkFolderTree = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: bookmarkFolderData, mutate: mutateBookmarkFolderData } = useSWRxCurrentUserBookmarkFolders();
+  const { data: bookmarkFolderData } = useSWRxBookamrkFolderAndChild(true);
+  const [activeElement, setActiveElement] = useState<string | null>(null);
 
 
+  const updateActiveElement = (parentId: string | null) => {
+    setActiveElement(parentId);
+  };
 
 
   if (bookmarkFolderData != null) {
   if (bookmarkFolderData != null) {
     return (
     return (
 
 
-      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group p-3`}>
+      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group p-3`}>
         {bookmarkFolderData.map((item) => {
         {bookmarkFolderData.map((item) => {
           return (
           return (
             <BookmarkFolderItem
             <BookmarkFolderItem
               key={item.bookmarkFolder._id}
               key={item.bookmarkFolder._id}
               bookmarkFolders={item}
               bookmarkFolders={item}
               isOpen={false}
               isOpen={false}
+              updateActiveElement={updateActiveElement}
+              isActive={item.bookmarkFolder._id === activeElement}
             />
             />
           );
           );
         })}
         })}

+ 6 - 0
packages/app/src/components/Theme/ThemeIsland.global.scss

@@ -127,5 +127,11 @@ $color-themelight: rgba(183, 226, 219, 1);
         @include mixins.button-outline-svg-icon-variant($gray-400, $bgcolor-sidebar);
         @include mixins.button-outline-svg-icon-variant($gray-400, $bgcolor-sidebar);
       }
       }
     }
     }
+    // Foldertree
+    .grw-foldertree {
+      .grw-foldertree-triangle-btn {
+        @include mixins.button-outline-svg-icon-variant($gray-400, $bgcolor-sidebar);
+      }
+    }
   }
   }
 }
 }

+ 7 - 4
packages/app/src/server/models/bookmark-folder.ts

@@ -15,7 +15,7 @@ export class InvalidParentBookmarkFolder extends ExtensibleCustomError {}
 
 
 export type BookmarkFolderItems = {
 export type BookmarkFolderItems = {
   bookmarkFolder: IBookmarkFolderDocument & HasObjectId
   bookmarkFolder: IBookmarkFolderDocument & HasObjectId
-  childCount: number
+  children: BookmarkFolderItems[]
 }
 }
 
 
 export type IBookmarkFolderDocument = {
 export type IBookmarkFolderDocument = {
@@ -32,7 +32,7 @@ export interface BookmarkFolderDocument extends Document {
 
 
 export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
 export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
   createByParameters(params: IBookmarkFolderDocument): IBookmarkFolderDocument
   createByParameters(params: IBookmarkFolderDocument): IBookmarkFolderDocument
-  findParentFolderByUserId(user: Types.ObjectId | string): BookmarkFolderDocument[]
+  findParentFolderByUserId(user: Types.ObjectId | string, parentId: Types.ObjectId | string | null): BookmarkFolderDocument[]
   findChildFolderById(parentBookmarkFolder: Types.ObjectId | string): Promise<BookmarkFolderDocument[]>
   findChildFolderById(parentBookmarkFolder: Types.ObjectId | string): Promise<BookmarkFolderDocument[]>
   deleteFolderAndChildren(bookmarkFolderId: string): {deletedCount: number}
   deleteFolderAndChildren(bookmarkFolderId: string): {deletedCount: number}
   updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string): BookmarkFolderDocument | null
   updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string): BookmarkFolderDocument | null
@@ -66,8 +66,11 @@ bookmarkFolderSchema.statics.createByParameters = async function(params: IBookma
   return bookmarkFolder;
   return bookmarkFolder;
 };
 };
 
 
-bookmarkFolderSchema.statics.findParentFolderByUserId = async function(userId: Types.ObjectId | string): Promise<BookmarkFolderDocument[]> {
-  const bookmarks = this.find({ owner: userId, parent: null }) as unknown as BookmarkFolderDocument[];
+bookmarkFolderSchema.statics.findParentFolderByUserId = async function(
+    userId: Types.ObjectId | string,
+    parentId: Types.ObjectId | string | null,
+): Promise<BookmarkFolderDocument[]> {
+  const bookmarks = this.find({ owner: userId, parent: parentId }) as unknown as BookmarkFolderDocument[];
   return bookmarks;
   return bookmarks;
 };
 };
 
 

+ 5 - 18
packages/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -44,28 +44,15 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
-  // List all main bookmark folders
-  router.get('/list', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    try {
-      const bookmarkFolders = await BookmarkFolder.findParentFolderByUserId(req.user?._id);
-      const bookmarkFolderItems = await Promise.all(bookmarkFolders.map(async bookmarkFolder => ({
-        bookmarkFolder,
-        childCount: await BookmarkFolder.countDocuments({ parent: bookmarkFolder._id }),
-      })));
-      return res.apiv3({ bookmarkFolderItems });
-    }
-    catch (err) {
-      return res.apiv3Err(err, 500);
-    }
-  });
-
-  router.get('/list-child/:parentId', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+  // List bookmark folders and child
+  router.get('/list/:parentId?', accessTokenParser, loginRequiredStrictly, async(req, res) => {
     const { parentId } = req.params;
     const { parentId } = req.params;
+    const _parentId = parentId != null ? parentId : null;
     try {
     try {
-      const bookmarkFolders = await BookmarkFolder.findChildFolderById(parentId);
+      const bookmarkFolders = await BookmarkFolder.findParentFolderByUserId(req.user?._id, _parentId);
       const bookmarkFolderItems = await Promise.all(bookmarkFolders.map(async bookmarkFolder => ({
       const bookmarkFolderItems = await Promise.all(bookmarkFolders.map(async bookmarkFolder => ({
         bookmarkFolder,
         bookmarkFolder,
-        childCount: await BookmarkFolder.countDocuments({ parent: bookmarkFolder._id }),
+        children: await BookmarkFolder.find({ parent: bookmarkFolder._id }),
       })));
       })));
       return res.apiv3({ bookmarkFolderItems });
       return res.apiv3({ bookmarkFolderItems });
     }
     }

+ 16 - 0
packages/app/src/stores/bookmark-folder.ts

@@ -0,0 +1,16 @@
+import { Nullable } from '@growi/core';
+import { SWRResponse } from 'swr';
+import useSWRImmutable from 'swr/immutable';
+
+import { apiv3Get } from '~/client/util/apiv3-client';
+import { BookmarkFolderItems } from '~/server/models/bookmark-folder';
+
+export const useSWRxBookamrkFolderAndChild = (isOpen: boolean, parentId?: Nullable<string>): SWRResponse<BookmarkFolderItems[], Error> => {
+  const _parentId = parentId == null ? '' : parentId;
+  return useSWRImmutable(
+    isOpen ? `/bookmark-folder/list/${_parentId}` : null,
+    endpoint => apiv3Get(endpoint).then((response) => {
+      return response.data.bookmarkFolderItems;
+    }),
+  );
+};

+ 0 - 19
packages/app/src/stores/bookmark.ts

@@ -3,7 +3,6 @@ import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
-import { BookmarkFolderItems } from '~/server/models/bookmark-folder';
 
 
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
@@ -39,21 +38,3 @@ export const useSWRxCurrentUserBookmarks = (pageNum?: Nullable<number>): SWRResp
     }),
     }),
   );
   );
 };
 };
-
-export const useSWRxCurrentUserBookmarkFolders = () : SWRResponse<BookmarkFolderItems[], Error> => {
-  return useSWRImmutable(
-    '/bookmark-folder/list',
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return response.data.bookmarkFolderItems;
-    }),
-  );
-};
-
-export const useSWRxChildBookmarkFolders = (isOpen: boolean, parentId: Nullable<string>): SWRResponse<BookmarkFolderItems[], Error> => {
-  return useSWRImmutable(
-    isOpen && parentId != null ? `/bookmark-folder/list-child/${parentId}` : null,
-    endpoint => apiv3Get(endpoint).then((response) => {
-      return response.data.bookmarkFolderItems;
-    }),
-  );
-};

+ 27 - 0
packages/app/src/styles/theme/_apply-colors-dark.scss

@@ -316,6 +316,33 @@ ul.pagination {
     }
     }
   }
   }
 
 
+  // Foldertree
+  .grw-foldertree {
+    @include override-list-group-item-for-pagetree(
+      $color-sidebar-context,
+      lighten($bgcolor-sidebar-context, 8%),
+      lighten($bgcolor-sidebar-context, 15%),
+      darken($color-sidebar-context, 15%),
+      darken($color-sidebar-context, 10%),
+      lighten($bgcolor-sidebar-context, 18%),
+      lighten($bgcolor-sidebar-context, 24%)
+    );
+    .grw-foldertree-triangle-btn {
+      @include mixins.button-outline-svg-icon-variant($secondary, $gray-200);
+    }
+    .btn-page-item-control {
+      @include button-outline-variant($gray-500, $gray-500, $secondary, transparent);
+      @include hover() {
+        background-color: lighten($bgcolor-sidebar-context, 20%);
+      }
+      &:not(:disabled):not(.disabled):active,
+      &:not(:disabled):not(.disabled).active {
+        background-color: lighten($bgcolor-sidebar-context, 34%);
+      }
+      box-shadow: none !important;
+    }
+  }
+
   // bookmarks
   // bookmarks
   .grw-bookmarks-list {
   .grw-bookmarks-list {
     @include override-list-group-item-for-pagetree(
     @include override-list-group-item-for-pagetree(

+ 16 - 0
packages/app/src/styles/theme/_apply-colors-light.scss

@@ -205,6 +205,22 @@ $dropdown-link-active-bg: $bgcolor-dropdown-link-active;
     }
     }
   }
   }
 
 
+  // Foldertree
+  .grw-foldertree {
+    @include override-list-group-item-for-pagetree(
+      $color-sidebar-context,
+      darken($bgcolor-sidebar-context, 5%),
+      darken($bgcolor-sidebar-context, 12%),
+      lighten($color-sidebar-context, 10%),
+      lighten($color-sidebar-context, 8%),
+      darken($bgcolor-sidebar-context, 15%),
+      darken($bgcolor-sidebar-context, 24%)
+    );
+    .grw-foldertree-triangle-btn {
+      @include mixins.button-outline-svg-icon-variant($gray-400, $primary);
+    }
+  }
+
   // bookmark
   // bookmark
   .grw-bookmarks-list {
   .grw-bookmarks-list {
     @include override-list-group-item-for-pagetree(
     @include override-list-group-item-for-pagetree(

+ 9 - 0
packages/app/src/styles/theme/_apply-colors.scss

@@ -292,6 +292,15 @@ ul.pagination {
       }
       }
     }
     }
   }
   }
+
+  .grw-foldertree {
+    .list-group-item {
+      .grw-foldertree-title-anchor {
+        color: inherit;
+      }
+    }
+  }
+
   .grw-pagetree-footer {
   .grw-pagetree-footer {
     .h5.grw-private-legacy-pages-anchor {
     .h5.grw-private-legacy-pages-anchor {
       color: inherit;
       color: inherit;

+ 4 - 2
packages/app/src/styles/theme/mixins/_list-group.scss

@@ -49,7 +49,8 @@
       }
       }
     }
     }
 
 
-    &.grw-pagetree-current-page-item {
+    &.grw-pagetree-current-page-item,
+    &.grw-foldertree-current-folder-item {
       background: $bgcolor-hover;
       background: $bgcolor-hover;
     }
     }
 
 
@@ -61,7 +62,8 @@
         background-color: $bgcolor-active;
         background-color: $bgcolor-active;
       }
       }
     }
     }
-    .grw-pagetree-title-anchor {
+    .grw-pagetree-title-anchor,
+    .grw-foldertree-title-anchor {
       .grw-sidebar-text-muted {
       .grw-sidebar-text-muted {
         color: rgba(desaturate($color, 50%), 0.6);
         color: rgba(desaturate($color, 50%), 0.6);
       }
       }