Browse Source

Create folder tree structure

https://youtrack.weseek.co.jp/issue/GW-7895
- Update bookmarkFolder type in BookmarkFolderItems
- Update findChildFolderById return type
- Update list child route return value
- Add SWR data fetching for children folder
- Update button triangle icon to expand bookmark folder tree
- Add folder icon in bookmark folder list
- Render children bookmark folder
- Update implementation of BookmarkFolderTree and BookmarkFolderItem component
Mudana-Grune 3 years ago
parent
commit
865b401ea8

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

@@ -13,7 +13,6 @@ import { usePageDeleteModal } from '~/stores/modal';
 
 
 
 
 import BookmarkFolder from './Bookmarks/BookmarkFolder';
 import BookmarkFolder from './Bookmarks/BookmarkFolder';
-import BookmarkFolderTree from './Bookmarks/BookmarkFolderTree';
 import BookmarkItem from './Bookmarks/BookmarkItem';
 import BookmarkItem from './Bookmarks/BookmarkItem';
 
 
 const Bookmarks = () : JSX.Element => {
 const Bookmarks = () : JSX.Element => {
@@ -25,7 +24,7 @@ const Bookmarks = () : JSX.Element => {
 
 
   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(null);
+  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) => {
@@ -105,8 +104,6 @@ const Bookmarks = () : JSX.Element => {
             onPressEnter={onPressEnterHandler}
             onPressEnter={onPressEnterHandler}
             folderName={folderName}
             folderName={folderName}
           />
           />
-          {/* TODO: List Bookmark Folder */}
-          <BookmarkFolderTree />
         </>
         </>
       )
       )
       }
       }

+ 3 - 0
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolder.tsx

@@ -5,6 +5,7 @@ import { useTranslation } from 'next-i18next';
 
 
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
 
 
+import BookmarkFolderTree from './BookmarkFolderTree';
 
 
 type Props = {
 type Props = {
   onClickNewFolder: () => void
   onClickNewFolder: () => void
@@ -18,6 +19,7 @@ const BookmarkFolder = (props: Props): JSX.Element => {
     onClickNewFolder, isRenameInputShown, folderName, onClickOutside, onPressEnter,
     onClickNewFolder, isRenameInputShown, folderName, onClickOutside, onPressEnter,
   } = props;
   } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
+
   return (
   return (
     <>
     <>
       <div className="col-8 mb-2 ">
       <div className="col-8 mb-2 ">
@@ -41,6 +43,7 @@ const BookmarkFolder = (props: Props): JSX.Element => {
           </div>
           </div>
         )
         )
       }
       }
+      <BookmarkFolderTree />
     </>
     </>
   );
   );
 };
 };

+ 56 - 17
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolderItem.tsx

@@ -1,47 +1,86 @@
-import { FC } from 'react';
+import {
+  FC, useCallback, useEffect, useState,
+} from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 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';
 
 
 
 
 type BookmarkFolderItemProps = {
 type BookmarkFolderItemProps = {
   bookmarkFolders: BookmarkFolderItems
   bookmarkFolders: BookmarkFolderItems
+  isOpen?: boolean
 }
 }
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
-  const { bookmarkFolders } = props;
+  const { bookmarkFolders, isOpen: _isOpen = false } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
   const hasChildren = bookmarkFolders.childCount > 0;
   const hasChildren = bookmarkFolders.childCount > 0;
+  const [currentParentFolder, setCurrentParentFolder] = useState<string | null>(null);
+  const [isActive, setIsActive] = useState<boolean>(false);
+  const [isOpen, setIsOpen] = useState(_isOpen);
+  const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxChildBookmarkFolders(isOpen, currentParentFolder);
+
+  useEffect(() => {
+    setCurrentParentFolder(bookmarkFolders.bookmarkFolder._id);
+  }, [bookmarkFolders]);
+
+  const onClickHandler = useCallback(async() => {
+    setCurrentParentFolder(bookmarkFolders.bookmarkFolder._id);
+    setIsOpen(!isOpen);
+    setIsActive(!isActive);
+    mutateChildBookmarkData();
+  }, [bookmarkFolders, isOpen, isActive, mutateChildBookmarkData]);
+
   return (
   return (
-    <div className="grw-pagetree-item-container" >
-      <li className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center">
+    <div id={`bookmark-folder-item-${bookmarkFolders.bookmarkFolder._id}`} className="grw-pagetree-item-container"
+    >
+      <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' : ''}`}
+      >
         <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 '}
-              onClick={() => {}}
+              className={`grw-pagetree-triangle-btn btn ${isOpen ? 'grw-pagetree-open' : ''}`}
+              onClick={onClickHandler}
             >
             >
               <div className="d-flex justify-content-center">
               <div className="d-flex justify-content-center">
                 <TriangleIcon />
                 <TriangleIcon />
               </div>
               </div>
             </button>
             </button>
           )}
           )}
-
-          {
-            <div className='grw-pagetree-title-anchor flex-grow-1'>
-              <p className={'text-truncate m-auto '}>{bookmarkFolders.bookmarkFolder.name}</p>
-            </div>
-          }
-          {hasChildren && (
-            <div className="grw-pagetree-count-wrapper">
-              <CountBadge count={bookmarkFolders.childCount } />
-            </div>
-          )}
         </div>
         </div>
+        {
+          <div>
+            <i className={`fa fa ${isOpen ? 'fa-folder-open-o' : 'fa-folder-o'} pr-2` } style={{ fontSize: '1.4em' }}></i>
+          </div>
+        }
+        {
+          <div className='grw-pagetree-title-anchor flex-grow-1'>
+            <p className={'text-truncate m-auto '}>{bookmarkFolders.bookmarkFolder.name}</p>
+          </div>
+        }
+        {hasChildren && (
+          <div className="grw-pagetree-count-wrapper">
+            <CountBadge count={bookmarkFolders.childCount } />
+          </div>
+        )}
+
       </li>
       </li>
+      {
+        isOpen && hasChildren && childBookmarkFolderData?.map(children => (
+          <div key={children.bookmarkFolder._id} className="grw-pagetree-item-children">
+            <BookmarkFolderItem
+              key={children.bookmarkFolder._id}
+              bookmarkFolders={children}
+              isOpen = {false}
+            />
+          </div>
+        ))
+      }
     </div>
     </div>
   );
   );
 };
 };

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

@@ -1,4 +1,4 @@
-import React, { useRef } from 'react';
+import React from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
@@ -8,18 +8,23 @@ import BookmarkFolderItem from './BookmarkFolderItem';
 
 
 import styles from './BookmarkFolderTree.module.scss';
 import styles from './BookmarkFolderTree.module.scss';
 
 
+
 const BookmarkFolderTree = (): JSX.Element => {
 const BookmarkFolderTree = (): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const rootFolderRef = useRef(null);
   const { data: bookmarkFolderData, mutate: mutateBookmarkFolderData } = useSWRxCurrentUserBookmarkFolders();
   const { data: bookmarkFolderData, mutate: mutateBookmarkFolderData } = useSWRxCurrentUserBookmarkFolders();
 
 
+
   if (bookmarkFolderData != null) {
   if (bookmarkFolderData != null) {
     return (
     return (
 
 
-      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group p-3`} ref={rootFolderRef}>
-        {bookmarkFolderData.map((item, index) => {
+      <ul className={`grw-pagetree ${styles['grw-pagetree']} list-group p-3`}>
+        {bookmarkFolderData.map((item) => {
           return (
           return (
-            <BookmarkFolderItem key={index} bookmarkFolders={item} />
+            <BookmarkFolderItem
+              key={item.bookmarkFolder._id}
+              bookmarkFolders={item}
+              isOpen={false}
+            />
           );
           );
         })}
         })}
       </ul>
       </ul>

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

@@ -1,4 +1,4 @@
-import { Ref, IUser } from '@growi/core';
+import { Ref, IUser, HasObjectId } from '@growi/core';
 import { isValidObjectId } from '@growi/core/src/utils/objectid-utils';
 import { isValidObjectId } from '@growi/core/src/utils/objectid-utils';
 import ExtensibleCustomError from 'extensible-custom-error';
 import ExtensibleCustomError from 'extensible-custom-error';
 import {
 import {
@@ -14,7 +14,7 @@ const logger = loggerFactory('growi:models:bookmark-folder');
 export class InvalidParentBookmarkFolder extends ExtensibleCustomError {}
 export class InvalidParentBookmarkFolder extends ExtensibleCustomError {}
 
 
 export type BookmarkFolderItems = {
 export type BookmarkFolderItems = {
-  bookmarkFolder: IBookmarkFolderDocument
+  bookmarkFolder: IBookmarkFolderDocument & HasObjectId
   childCount: number
   childCount: number
 }
 }
 
 
@@ -33,7 +33,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): BookmarkFolderDocument[]
-  findChildFolderById(parentBookmarkFolder: Types.ObjectId | string): Promise<IBookmarkFolderDocument[]>
+  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
 }
 }

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

@@ -63,7 +63,11 @@ module.exports = (crowi) => {
     const { parentId } = req.params;
     const { parentId } = req.params;
     try {
     try {
       const bookmarkFolders = await BookmarkFolder.findChildFolderById(parentId);
       const bookmarkFolders = await BookmarkFolder.findChildFolderById(parentId);
-      return res.apiv3({ bookmarkFolders });
+      const bookmarkFolderItems = await Promise.all(bookmarkFolders.map(async bookmarkFolder => ({
+        bookmarkFolder,
+        childCount: await BookmarkFolder.countDocuments({ parent: bookmarkFolder._id }),
+      })));
+      return res.apiv3({ bookmarkFolderItems });
     }
     }
     catch (err) {
     catch (err) {
       return res.apiv3Err(err, 500);
       return res.apiv3Err(err, 500);

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

@@ -48,3 +48,12 @@ export const useSWRxCurrentUserBookmarkFolders = () : SWRResponse<BookmarkFolder
     }),
     }),
   );
   );
 };
 };
+
+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;
+    }),
+  );
+};