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

Merge pull request #7426 from weseek/feat/gw7920-save-and-display-bookmarks-outside-folder

feat: gw7920 save and display bookmarks outside folder
Ryoji Shimizu 3 лет назад
Родитель
Сommit
f89bea7111
22 измененных файлов с 605 добавлено и 317 удалено
  1. 4 1
      packages/app/public/static/locales/en_US/translation.json
  2. 4 1
      packages/app/public/static/locales/ja_JP/translation.json
  3. 4 1
      packages/app/public/static/locales/zh_CN/translation.json
  4. 1 5
      packages/app/src/components/BookmarkButtons.tsx
  5. 69 85
      packages/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  6. 110 53
      packages/app/src/components/Bookmarks/BookmarkFolderMenu.tsx
  7. 46 34
      packages/app/src/components/Bookmarks/BookmarkFolderMenuItem.tsx
  8. 18 0
      packages/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  9. 138 15
      packages/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  10. 12 16
      packages/app/src/components/Bookmarks/BookmarkItem.tsx
  11. 1 1
      packages/app/src/components/Sidebar/Bookmarks/BookmarkContents.tsx
  12. 31 16
      packages/app/src/components/UsersHomePageFooter.module.scss
  13. 3 2
      packages/app/src/components/UsersHomePageFooter.tsx
  14. 7 0
      packages/app/src/interfaces/bookmark-info.ts
  15. 85 35
      packages/app/src/server/models/bookmark-folder.ts
  16. 21 12
      packages/app/src/server/routes/apiv3/bookmark-folder.ts
  17. 6 30
      packages/app/src/server/routes/apiv3/bookmarks.js
  18. 3 4
      packages/app/src/stores/bookmark-folder.ts
  19. 5 6
      packages/app/src/stores/bookmark.ts
  20. 4 0
      packages/app/src/styles/molecules/_bookmark-folder-tree.scss
  21. 16 0
      packages/app/src/styles/theme/_apply-colors-dark.scss
  22. 17 0
      packages/app/src/styles/theme/_apply-colors-light.scss

+ 4 - 1
packages/app/public/static/locales/en_US/translation.json

@@ -823,7 +823,10 @@
     },
     },
     "input_placeholder": "Input folder name",
     "input_placeholder": "Input folder name",
     "new_folder": "New Folder",
     "new_folder": "New Folder",
-    "delete": "Delete Folder"
+    "delete": "Delete Folder",
+    "drop_item_here": "Drop item here",
+    "cancel_bookmark": "Un-bookmark this page",
+    "bookmark_this_page": "Bookmark this page"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",
     "page_tree_not_avaliable" : "Page tree feature is not available yet.",

+ 4 - 1
packages/app/public/static/locales/ja_JP/translation.json

@@ -823,7 +823,10 @@
     },
     },
     "input_placeholder": "フォルダ名を入力してください`",
     "input_placeholder": "フォルダ名を入力してください`",
     "new_folder": "新しいフォルダ",
     "new_folder": "新しいフォルダ",
-    "delete": "フォルダを削除"
+    "delete": "フォルダを削除",
+    "drop_item_here": "ここにアイテムをドロップ",
+    "cancel_bookmark": "このページのブックマークを解除",
+    "bookmark_this_page": "このページをブックマークして"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",
     "page_tree_not_avaliable" : "Page Tree 機能は現在使用できません。",

+ 4 - 1
packages/app/public/static/locales/zh_CN/translation.json

@@ -826,7 +826,10 @@
     },
     },
     "input_placeholder": "输入文件夹名称",
     "input_placeholder": "输入文件夹名称",
     "new_folder": "新建文件夹",
     "new_folder": "新建文件夹",
-    "delete": "删除文件夹"
+    "delete": "删除文件夹",
+    "drop_item_here": "将项目放在这里",
+    "cancel_bookmark": "取消收藏此页面",
+    "bookmark_this_page": "收藏此页"
   },
   },
   "v5_page_migration": {
   "v5_page_migration": {
     "page_tree_not_avaliable": "Page Tree 功能不可用",
     "page_tree_not_avaliable": "Page Tree 功能不可用",

+ 1 - 5
packages/app/src/components/BookmarkButtons.tsx

@@ -44,12 +44,8 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
     if (isGuestUser) {
     if (isGuestUser) {
       return 'Not available for guest';
       return 'Not available for guest';
     }
     }
-
-    if (bookmarkInfo?.isBookmarked) {
-      return 'tooltip.cancel_bookmark';
-    }
     return 'tooltip.bookmark';
     return 'tooltip.bookmark';
-  }, [isGuestUser, bookmarkInfo]);
+  }, [isGuestUser]);
 
 
 
 
   return (
   return (

+ 69 - 85
packages/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -1,5 +1,5 @@
 import {
 import {
-  FC, useCallback, useEffect, useState,
+  FC, useCallback, useState,
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
@@ -10,10 +10,10 @@ import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import FolderIcon from '~/components/Icons/FolderIcon';
 import FolderIcon from '~/components/Icons/FolderIcon';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
-import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { BookmarkFolderItems, DragItemType, DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
 import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { onDeletedBookmarkFolderFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { onDeletedBookmarkFolderFunction, OnDeletedFunction } from '~/interfaces/ui';
-import { useSWRBookmarkInfo } from '~/stores/bookmark';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useBookmarkFolderDeleteModal, usePageDeleteModal } from '~/stores/modal';
 import { useBookmarkFolderDeleteModal, usePageDeleteModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -30,7 +30,11 @@ type BookmarkFolderItemProps = {
   root: string
   root: string
   isUserHomePage?: boolean
   isUserHomePage?: boolean
 }
 }
+
+type DragItemDataType = BookmarkFolderItemProps & IPageHasId;
+
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
+  const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const {
   const {
     bookmarkFolder, isOpen: _isOpen = false, level, root, isUserHomePage,
     bookmarkFolder, isOpen: _isOpen = false, level, root, isUserHomePage,
   } = props;
   } = props;
@@ -39,11 +43,11 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
   const {
   const {
     name, _id: folderId, children, parent, bookmarks,
     name, _id: folderId, children, parent, bookmarks,
   } = bookmarkFolder;
   } = bookmarkFolder;
-  const [currentChildren, setCurrentChildren] = useState<BookmarkFolderItems[]>();
+
   const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
   const [targetFolder, setTargetFolder] = useState<string | null>(folderId);
   const [isOpen, setIsOpen] = useState(_isOpen);
   const [isOpen, setIsOpen] = useState(_isOpen);
-  const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(targetFolder);
-  const { mutate: mutateParentBookmarkFolder } = useSWRxBookamrkFolderAndChild(parent);
+  const { mutate: mutateBookmarkData } = useSWRxBookamrkFolderAndChild();
+  const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
   const [isRenameAction, setIsRenameAction] = useState<boolean>(false);
   const [isRenameAction, setIsRenameAction] = useState<boolean>(false);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
@@ -51,51 +55,27 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteModal } = usePageDeleteModal();
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
 
 
-  useEffect(() => {
-    if (childBookmarkFolderData != null) {
-      mutateChildBookmarkData();
-      setCurrentChildren(childBookmarkFolderData);
-    }
-  }, [childBookmarkFolderData, mutateChildBookmarkData]);
-
   const hasChildren = useCallback((): boolean => {
   const hasChildren = useCallback((): boolean => {
-    if (currentChildren != null && currentChildren.length > children.length) {
-      return currentChildren.length > 0;
-    }
-    return children.length > 0;
-  }, [children.length, currentChildren]);
+    return children != null && children.length > 0;
+  }, [children]);
 
 
   const loadChildFolder = useCallback(async() => {
   const loadChildFolder = useCallback(async() => {
     setIsOpen(!isOpen);
     setIsOpen(!isOpen);
     setTargetFolder(folderId);
     setTargetFolder(folderId);
   }, [folderId, isOpen]);
   }, [folderId, isOpen]);
 
 
-  const loadParent = useCallback(async() => {
-    if (!isRenameAction) {
-      if (parent != null) {
-        await mutateParentBookmarkFolder();
-      }
-      // Reload root folder structure
-      setTargetFolder(null);
-    }
-    else {
-      await mutateParentBookmarkFolder();
-    }
-
-  }, [isRenameAction, mutateParentBookmarkFolder, parent]);
-
   // Rename  for bookmark folder handler
   // Rename  for bookmark folder handler
   const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForRename = useCallback(async(folderName: string) => {
     try {
     try {
       await apiv3Put('/bookmark-folder', { bookmarkFolderId: folderId, name: folderName, parent });
       await apiv3Put('/bookmark-folder', { bookmarkFolderId: folderId, name: folderName, parent });
-      loadParent();
+      mutateBookmarkData();
       setIsRenameAction(false);
       setIsRenameAction(false);
       toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
       toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [folderId, loadParent, parent, t]);
+  }, [folderId, mutateBookmarkData, parent, t]);
 
 
   // Create new folder / subfolder handler
   // Create new folder / subfolder handler
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
@@ -103,7 +83,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
       await apiv3Post('/bookmark-folder', { name: folderName, parent: targetFolder });
       await apiv3Post('/bookmark-folder', { name: folderName, parent: targetFolder });
       setIsOpen(true);
       setIsOpen(true);
       setIsCreateAction(false);
       setIsCreateAction(false);
-      mutateChildBookmarkData();
+      mutateBookmarkData();
       toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
       toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
 
 
     }
     }
@@ -111,7 +91,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
       toastError(err);
       toastError(err);
     }
     }
 
 
-  }, [mutateChildBookmarkData, t, targetFolder]);
+  }, [mutateBookmarkData, t, targetFolder]);
 
 
 
 
   const onClickPlusButton = useCallback(async(e) => {
   const onClickPlusButton = useCallback(async(e) => {
@@ -135,24 +115,24 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
       else {
       else {
         toastSuccess(t('deleted_pages', { path }));
         toastSuccess(t('deleted_pages', { path }));
       }
       }
-      mutateParentBookmarkFolder();
+      mutateBookmarkData();
       mutateBookmarkInfo();
       mutateBookmarkInfo();
     };
     };
     openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
     openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
-  }, [mutateBookmarkInfo, mutateParentBookmarkFolder, openDeleteModal, t]);
+  }, [mutateBookmarkInfo, mutateBookmarkData, openDeleteModal, t]);
 
 
   const onUnbookmarkHandler = useCallback(() => {
   const onUnbookmarkHandler = useCallback(() => {
-    mutateParentBookmarkFolder();
+    mutateBookmarkData();
     mutateBookmarkInfo();
     mutateBookmarkInfo();
-  }, [mutateBookmarkInfo, mutateParentBookmarkFolder]);
+  }, [mutateBookmarkInfo, mutateBookmarkData]);
 
 
   const [, bookmarkFolderDragRef] = useDrag({
   const [, bookmarkFolderDragRef] = useDrag({
-    type: 'FOLDER',
+    type: DRAG_ITEM_TYPE.FOLDER,
     item: props,
     item: props,
-    end: (item, monitor) => {
+    end: (_item, monitor) => {
       const dropResult = monitor.getDropResult();
       const dropResult = monitor.getDropResult();
       if (dropResult != null) {
       if (dropResult != null) {
-        mutateParentBookmarkFolder();
+        mutateBookmarkData();
       }
       }
     },
     },
     collect: monitor => ({
     collect: monitor => ({
@@ -162,59 +142,63 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
   });
   });
 
 
 
 
-  const folderItemDropHandler = async(item: BookmarkFolderItemProps) => {
-    try {
-      await apiv3Put('/bookmark-folder', { bookmarkFolderId: item.bookmarkFolder._id, name: item.bookmarkFolder.name, parent: bookmarkFolder._id });
-      await mutateChildBookmarkData();
-      toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
+  const itemDropHandler = async(item: DragItemDataType, dragItemType: string | symbol | null) => {
+    if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
+      try {
+        await apiv3Put('/bookmark-folder', { bookmarkFolderId: item.bookmarkFolder._id, name: item.bookmarkFolder.name, parent: bookmarkFolder._id });
+        mutateBookmarkData();
+        toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
+      }
+      catch (err) {
+        toastError(err);
+      }
     }
     }
-    catch (err) {
-      toastError(err);
+    else {
+      try {
+        await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: item._id, folderId: bookmarkFolder._id });
+        mutateBookmarkData();
+        await mutateUserBookmarks();
+        toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+      }
+      catch (err) {
+        toastError(err);
+      }
     }
     }
   };
   };
 
 
-  const bookmarkItemDropHandler = useCallback(async(item: IPageHasId) => {
-    try {
-      await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: item._id, folderId: bookmarkFolder._id });
-      mutateParentBookmarkFolder();
-      toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
-    }
-    catch (err) {
-      toastError(err);
+  const isDroppable = (item: DragItemDataType, targetRoot: string, targetLevel: number, type: string | null| symbol): boolean => {
+    if (type === DRAG_ITEM_TYPE.FOLDER) {
+      if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
+        return false;
+      }
+      return item.root !== targetRoot || item.level >= targetLevel;
     }
     }
-
-  }, [bookmarkFolder._id, mutateParentBookmarkFolder, t]);
-
-
-  const isDroppable = (item: BookmarkFolderItemProps, targetRoot: string, targetLevel: number): boolean => {
-    if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
+    const bookmarks = bookmarkFolder.bookmarks;
+    const isBookmarkExists = bookmarks.filter(bookmark => bookmark.page._id === item._id).length > 0;
+    if (isBookmarkExists) {
       return false;
       return false;
     }
     }
-    return item.root !== targetRoot || item.level >= targetLevel;
+    return true;
   };
   };
 
 
-  const [, bookmarkFolderDropRef] = useDrop(() => ({
-    accept: 'FOLDER',
-    drop: folderItemDropHandler,
-    canDrop: (item) => {
-      // Implement isDropable function & improve
-      return isDroppable(item, root, level);
+  const [{ isOver }, dropRef] = useDrop(() => ({
+    accept: acceptedTypes,
+    drop: (item: DragItemDataType, monitor) => {
+      const itemType = monitor.getItemType();
+      itemDropHandler(item, itemType);
+    },
+    canDrop: (item: DragItemDataType, monitor) => {
+      const itemType = monitor.getItemType();
+      return isDroppable(item, root, level, itemType);
     },
     },
     collect: monitor => ({
     collect: monitor => ({
-      isOver: monitor.isOver(),
+      isOver: monitor.isOver({ shallow: true }) && monitor.canDrop(),
     }),
     }),
   }));
   }));
 
 
-  const [, bookmarkItemDropRef] = useDrop(() => ({
-    accept: 'BOOKMARK',
-    drop: bookmarkItemDropHandler,
-    collect: monitor => ({
-      isOver: monitor.isOver(),
-    }),
-  }));
 
 
   const renderChildFolder = () => {
   const renderChildFolder = () => {
-    return isOpen && currentChildren?.map((childFolder) => {
+    return isOpen && children?.map((childFolder) => {
       return (
       return (
         <div key={childFolder._id} className="grw-foldertree-item-children">
         <div key={childFolder._id} className="grw-foldertree-item-children">
           <BookmarkFolderItem
           <BookmarkFolderItem
@@ -237,7 +221,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
           bookmarkedPage={bookmark.page}
           bookmarkedPage={bookmark.page}
           key={bookmark._id}
           key={bookmark._id}
           onUnbookmarked={onUnbookmarkHandler}
           onUnbookmarked={onUnbookmarkHandler}
-          onRenamed={mutateParentBookmarkFolder}
+          onRenamed={mutateBookmarkData}
           onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
           onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
           parentFolder={bookmarkFolder}
           parentFolder={bookmarkFolder}
         />
         />
@@ -254,8 +238,8 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
       if (typeof folderId !== 'string') {
       if (typeof folderId !== 'string') {
         return;
         return;
       }
       }
-      loadParent();
       mutateBookmarkInfo();
       mutateBookmarkInfo();
+      mutateBookmarkData();
       toastSuccess(t('toaster.delete_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
       toastSuccess(t('toaster.delete_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
     };
     };
 
 
@@ -263,13 +247,13 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
       return;
       return;
     }
     }
     openDeleteBookmarkFolderModal(bookmarkFolder, { onDeleted: bookmarkFolderDeleteHandler });
     openDeleteBookmarkFolderModal(bookmarkFolder, { onDeleted: bookmarkFolderDeleteHandler });
-  }, [bookmarkFolder, loadParent, mutateBookmarkInfo, openDeleteBookmarkFolderModal, t]);
+  }, [bookmarkFolder, mutateBookmarkData, mutateBookmarkInfo, openDeleteBookmarkFolderModal, t]);
 
 
 
 
   return (
   return (
     <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
     <div id={`grw-bookmark-folder-item-${folderId}`} className="grw-foldertree-item-container">
-      <li ref={(c) => { bookmarkFolderDragRef(c); bookmarkFolderDropRef(c); bookmarkItemDropRef(c) }}
-        className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center"
+      <li ref={(c) => { bookmarkFolderDragRef(c); dropRef(c) }}
+        className={`${isOver ? 'grw-accept-drop-item' : ''} list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center`}
         onClick={loadChildFolder}
         onClick={loadChildFolder}
       >
       >
         <div className="grw-triangle-container d-flex justify-content-center">
         <div className="grw-triangle-container d-flex justify-content-center">

+ 110 - 53
packages/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -7,9 +7,10 @@ import {
   DropdownItem, DropdownMenu, UncontrolledDropdown,
   DropdownItem, DropdownMenu, UncontrolledDropdown,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiv3Post } from '~/client/util/apiv3-client';
+import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { useSWRBookmarkInfo } from '~/stores/bookmark';
+import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
 
 
@@ -30,23 +31,65 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
   const { data: bookmarkFolders, mutate: mutateBookmarkFolderData } = useSWRxBookamrkFolderAndChild();
   const { data: bookmarkFolders, mutate: mutateBookmarkFolderData } = useSWRxBookamrkFolderAndChild();
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
-  const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
+  const { data: userBookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
+  const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
+  const isBookmarked = userBookmarkInfo?.isBookmarked;
+  const [isOpen, setIsOpen] = useState(false);
 
 
 
 
+  const toggleBookmarkHandler = useCallback(async() => {
+
+    try {
+      await apiv3Put('/bookmark-folder/update-bookmark', { pageId: currentPage?._id, status: isBookmarked });
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+
+  }, [currentPage, isBookmarked]);
+
   const onClickNewBookmarkFolder = useCallback(() => {
   const onClickNewBookmarkFolder = useCallback(() => {
     setIsCreateAction(true);
     setIsCreateAction(true);
   }, []);
   }, []);
 
 
-  const toggleHandler = useCallback(() => {
+  const onUnbookmarkHandler = useCallback(async() => {
+    await toggleBookmarkHandler();
+    mutateUserBookmarks();
+    mutateBookmarkInfo();
+    mutateBookmarkFolderData();
+    setSelectedItem(null);
+    toastSuccess(t('toaster.delete_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+  }, [mutateBookmarkFolderData, mutateBookmarkInfo, mutateUserBookmarks, t, toggleBookmarkHandler]);
+
+  const toggleHandler = useCallback(async() => {
+    setIsOpen(!isOpen);
     mutateBookmarkFolderData();
     mutateBookmarkFolderData();
-    bookmarkFolders?.forEach((bookmarkFolder) => {
-      bookmarkFolder.bookmarks.forEach((bookmark) => {
-        if (bookmark.page._id === currentPage?._id) {
-          setSelectedItem(bookmarkFolder._id);
-        }
+    if (isOpen && bookmarkFolders != null) {
+      bookmarkFolders?.forEach((bookmarkFolder) => {
+        bookmarkFolder.bookmarks.forEach((bookmark) => {
+          if (bookmark.page._id === currentPage?._id) {
+            if (bookmark.page._id === currentPage?._id) {
+              setSelectedItem(bookmarkFolder._id);
+            }
+          }
+        });
       });
       });
-    });
-  }, [bookmarkFolders, currentPage?._id, mutateBookmarkFolderData]);
+    }
+    if (!isOpen && !isBookmarked) {
+      try {
+        toggleBookmarkHandler();
+        mutateUserBookmarks();
+        mutateBookmarkInfo();
+        setSelectedItem(null);
+        toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+      }
+      catch (err) {
+        toastError(err);
+      }
+    }
+  }, [isOpen, mutateBookmarkFolderData, bookmarkFolders, isBookmarked, currentPage?._id, toggleBookmarkHandler, mutateUserBookmarks, mutateBookmarkInfo, t]);
+
 
 
   const isBookmarkFolderExists = useCallback((): boolean => {
   const isBookmarkFolderExists = useCallback((): boolean => {
     if (bookmarkFolders && bookmarkFolders.length > 0) {
     if (bookmarkFolders && bookmarkFolders.length > 0) {
@@ -70,10 +113,14 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
 
 
   const onMenuItemClickHandler = useCallback(async(itemId: string) => {
   const onMenuItemClickHandler = useCallback(async(itemId: string) => {
     try {
     try {
+      if (isBookmarked) {
+        await toggleBookmarkHandler();
+      }
       await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: itemId });
       await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: itemId });
-
+      const toaster = isBookmarked ? 'toaster.update_successed' : 'toaster.add_succeeded';
+      toastSuccess(t(toaster, { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
       mutateBookmarkInfo();
       mutateBookmarkInfo();
-      toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+      mutateUserBookmarks();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -81,53 +128,63 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
 
 
     mutateBookmarkFolderData();
     mutateBookmarkFolderData();
     setSelectedItem(itemId);
     setSelectedItem(itemId);
-  }, [currentPage?._id, mutateBookmarkFolderData, mutateBookmarkInfo, t]);
-
-  const renderBookmarkMenuItem = useCallback(() => {
-    return (
-      <>
-        { isCreateAction ? (
-          <div className='mx-2'>
-            <BookmarkFolderNameInput
-              onClickOutside={() => setIsCreateAction(false)}
-              onPressEnter={onPressEnterHandlerForCreate}
-            />
-          </div>
-        ) : (
-          <DropdownItem toggle={false} onClick={onClickNewBookmarkFolder} className='grw-bookmark-folder-menu-item'>
-            <FolderIcon isOpen={false}/>
-            <span className="mx-2 ">{t('bookmark_folder.new_folder')}</span>
-          </DropdownItem>
-        )}
-        { isBookmarkFolderExists() && (
-          <>
-            <DropdownItem divider />
-            {bookmarkFolders?.map(folder => (
-              <div key={folder._id} >
-                {
+  }, [currentPage?._id, isBookmarked, mutateBookmarkFolderData, mutateBookmarkInfo, mutateUserBookmarks, toggleBookmarkHandler, t]);
+
+
+  const renderBookmarkMenuItem = (child ?:BookmarkFolderItems[]) => {
+    if (!child) {
+      return (
+        <>
+          { isBookmarked && (
+            <>
+              <DropdownItem toggle={false} onClick={onUnbookmarkHandler} className={'grw-bookmark-folder-menu-item text-danger'}>
+                <i className="fa fa-bookmark"></i> <span className="mx-2 ">
+                  {t('bookmark_folder.cancel_bookmark') }
+                </span>
+              </DropdownItem>
+              <DropdownItem divider />
+            </>)
+
+          }
+
+          { isCreateAction ? (
+            <div className='mx-2'>
+              <BookmarkFolderNameInput
+                onClickOutside={() => setIsCreateAction(false)}
+                onPressEnter={onPressEnterHandlerForCreate}
+              />
+            </div>
+          ) : (
+            <DropdownItem toggle={false} onClick={onClickNewBookmarkFolder} className='grw-bookmark-folder-menu-item'>
+              <FolderIcon isOpen={false}/>
+              <span className="mx-2 ">{t('bookmark_folder.new_folder')}</span>
+            </DropdownItem>
+          )}
+          {isBookmarkFolderExists() && (
+            <>
+              <DropdownItem divider />
+              {bookmarkFolders?.map(folder => (
+                <div key={folder._id}>
                   <div className='dropdown-item grw-bookmark-folder-menu-item' tabIndex={0} role="menuitem" onClick={() => onMenuItemClickHandler(folder._id)}>
                   <div className='dropdown-item grw-bookmark-folder-menu-item' tabIndex={0} role="menuitem" onClick={() => onMenuItemClickHandler(folder._id)}>
                     <BookmarkFolderMenuItem
                     <BookmarkFolderMenuItem
                       item={folder}
                       item={folder}
                       isSelected={selectedItem === folder._id}
                       isSelected={selectedItem === folder._id}
                       onSelectedChild={() => setSelectedItem(null)}
                       onSelectedChild={() => setSelectedItem(null)}
                     />
                     />
+                    {isOpen && (
+                      <div className="bookmark-folder-submenu">
+                        {renderBookmarkMenuItem(folder.children)}
+                      </div>
+                    )}
                   </div>
                   </div>
-                }
-              </div>
-            ))}
-          </>
-        )}
-      </>
-    );
-  }, [bookmarkFolders,
-      isBookmarkFolderExists,
-      isCreateAction,
-      onClickNewBookmarkFolder,
-      onMenuItemClickHandler,
-      onPressEnterHandlerForCreate,
-      t,
-      selectedItem,
-  ]);
+                </div>
+              ))}
+            </>
+          )}
+        </>
+      );
+    }
+  };
 
 
   return (
   return (
     <UncontrolledDropdown
     <UncontrolledDropdown

+ 46 - 34
packages/app/src/components/Bookmarks/BookmarkFolderMenuItem.tsx

@@ -6,11 +6,11 @@ import {
   DropdownMenu, DropdownToggle, UncontrolledDropdown, UncontrolledTooltip,
   DropdownMenu, DropdownToggle, UncontrolledDropdown, UncontrolledTooltip,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiv3Post } from '~/client/util/apiv3-client';
+import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
 import { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
-import { useSWRBookmarkInfo } from '~/stores/bookmark';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -32,19 +32,24 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
   } = props;
   } = props;
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);
-  const [currentChildFolders, setCurrentChildFolders] = useState<BookmarkFolderItems[]>();
-  const { data: childFolders, mutate: mutateChildFolders } = useSWRxBookamrkFolderAndChild(item._id);
-  const { mutate: mutateParentFolders } = useSWRxBookamrkFolderAndChild(item.parent);
+  const { mutate: mutateBookamrkData } = useSWRxBookamrkFolderAndChild();
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
-  const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
+  const { data: userBookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
+  const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
+
+  const isBookmarked = userBookmarkInfo?.isBookmarked;
+
+  const hasChildren = useCallback((): boolean => {
+    return item.children.length > 0;
+  }, [item.children.length]);
 
 
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
     try {
       await apiv3Post('/bookmark-folder', { name: folderName, parent: item._id });
       await apiv3Post('/bookmark-folder', { name: folderName, parent: item._id });
-      await mutateChildFolders();
+      await mutateBookamrkData();
       setIsCreateAction(false);
       setIsCreateAction(false);
       toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
       toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
     }
     }
@@ -52,23 +57,19 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
       toastError(err);
       toastError(err);
     }
     }
 
 
-  }, [item, mutateChildFolders, t]);
-
+  }, [item, mutateBookamrkData, t]);
 
 
   useEffect(() => {
   useEffect(() => {
-    if (isOpen && childFolders != null) {
-      mutateChildFolders();
-      setCurrentChildFolders(childFolders);
-    }
-    currentChildFolders?.forEach((bookmarkFolder) => {
-      bookmarkFolder.bookmarks.forEach((bookmark) => {
-        if (bookmark.page._id === currentPage?._id) {
-          setSelectedItem(bookmarkFolder._id);
-        }
+    if (isOpen) {
+      item.children?.forEach((bookmarkFolder) => {
+        bookmarkFolder.bookmarks.forEach((bookmark) => {
+          if (bookmark.page._id === currentPage?._id) {
+            setSelectedItem(bookmarkFolder._id);
+          }
+        });
       });
       });
-    });
-
-  }, [childFolders, currentChildFolders, currentPage?._id, isOpen, item, mutateChildFolders, mutateParentFolders]);
+    }
+  }, [currentPage?._id, isOpen, item.children]);
 
 
   const onClickNewBookmarkFolder = useCallback((e) => {
   const onClickNewBookmarkFolder = useCallback((e) => {
     e.stopPropagation();
     e.stopPropagation();
@@ -92,7 +93,7 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
         return;
         return;
       }
       }
       mutateBookmarkInfo();
       mutateBookmarkInfo();
-      mutateParentFolders();
+      mutateBookamrkData();
       toastSuccess(t('toaster.delete_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
       toastSuccess(t('toaster.delete_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
     };
     };
 
 
@@ -100,28 +101,32 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
       return;
       return;
     }
     }
     openDeleteBookmarkFolderModal(item, { onDeleted: bookmarkFolderDeleteHandler });
     openDeleteBookmarkFolderModal(item, { onDeleted: bookmarkFolderDeleteHandler });
-  }, [item, mutateBookmarkInfo, mutateParentFolders, openDeleteBookmarkFolderModal, t]);
+  }, [item, mutateBookamrkData, mutateBookmarkInfo, openDeleteBookmarkFolderModal, t]);
 
 
   const onClickChildMenuItemHandler = useCallback(async(e, item) => {
   const onClickChildMenuItemHandler = useCallback(async(e, item) => {
     e.stopPropagation();
     e.stopPropagation();
-    mutateBookmarkInfo();
     onSelectedChild();
     onSelectedChild();
     try {
     try {
+      if (isBookmarked) {
+        await apiv3Put('/bookmark-folder/update-bookmark', { pageId: currentPage?._id, status: isBookmarked });
+      }
       await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: item._id });
       await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: item._id });
-      toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
-      mutateParentFolders();
-      mutateChildFolders();
+      const toaster = isBookmarked ? 'toaster.update_successed' : 'toaster.add_succeeded';
+      toastSuccess(t(toaster, { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+      mutateUserBookmarks();
+      mutateBookamrkData();
       setSelectedItem(item._id);
       setSelectedItem(item._id);
+      mutateBookmarkInfo();
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [mutateBookmarkInfo, onSelectedChild, currentPage?._id, mutateParentFolders, mutateChildFolders, t]);
+  }, [onSelectedChild, isBookmarked, mutateBookamrkData, mutateBookmarkInfo, currentPage?._id, mutateUserBookmarks, t]);
 
 
   const renderBookmarkSubMenuItem = useCallback(() => {
   const renderBookmarkSubMenuItem = useCallback(() => {
     return (
     return (
       <>
       <>
-        {childFolders != null && (
+        {isOpen && (
           <DropdownMenu className='m-0'>
           <DropdownMenu className='m-0'>
             {isCreateAction ? (
             {isCreateAction ? (
               <div className='mx-2' onClick={e => e.stopPropagation()}>
               <div className='mx-2' onClick={e => e.stopPropagation()}>
@@ -137,11 +142,10 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
               </DropdownItem>
               </DropdownItem>
             )}
             )}
 
 
-            {currentChildFolders && currentChildFolders?.length > 0 && (<DropdownItem divider />)}
+            {hasChildren() && (<DropdownItem divider />)}
 
 
-            {currentChildFolders?.map(child => (
+            {item.children?.map(child => (
               <div key={child._id} >
               <div key={child._id} >
-
                 <div
                 <div
                   className='dropdown-item grw-bookmark-folder-menu-item'
                   className='dropdown-item grw-bookmark-folder-menu-item'
                   tabIndex={0} role="menuitem"
                   tabIndex={0} role="menuitem"
@@ -158,7 +162,15 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
         )}
         )}
       </>
       </>
     );
     );
-  }, [childFolders, currentChildFolders, isCreateAction, onClickChildMenuItemHandler, onClickNewBookmarkFolder, onPressEnterHandlerForCreate, selectedItem, t]);
+  }, [hasChildren,
+      isCreateAction,
+      isOpen, item.children,
+      onClickChildMenuItemHandler,
+      onClickNewBookmarkFolder,
+      onPressEnterHandlerForCreate,
+      t,
+      selectedItem,
+  ]);
 
 
   return (
   return (
     <>
     <>
@@ -196,7 +208,7 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
           onClick={e => e.stopPropagation()}
           onClick={e => e.stopPropagation()}
           onMouseEnter={onMouseEnterHandler}
           onMouseEnter={onMouseEnterHandler}
         >
         >
-          {childFolders && childFolders?.length > 0
+          {hasChildren()
             ? <TriangleIcon />
             ? <TriangleIcon />
             : (
             : (
               <i className="icon-plus d-block pl-0" />
               <i className="icon-plus d-block pl-0" />

+ 18 - 0
packages/app/src/components/Bookmarks/BookmarkFolderTree.module.scss

@@ -1 +1,19 @@
 @use '~/styles/molecules/bookmark-folder-tree';
 @use '~/styles/molecules/bookmark-folder-tree';
+
+.grw-folder-tree-container :global {
+  .grw-foldertree-item-container, .grw-drop-item-area {
+    & .grw-accept-drop-item {
+      border-style: dashed !important;
+      border-width: 0.15rem !important;
+    }
+  }
+
+  .grw-drop-item-area {
+    min-height: 90vh;
+    padding: 1rem;
+
+    & .grw-accept-drop-item {
+      padding: 0.7rem;
+    }
+  }
+}

+ 138 - 15
packages/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -1,7 +1,21 @@
 
 
+import { useCallback } from 'react';
+
+import { useTranslation } from 'next-i18next';
+import { useDrop } from 'react-dnd';
+
+import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
+import { BookmarkFolderItems, DragItemType, DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
+import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
+import { OnDeletedFunction } from '~/interfaces/ui';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
+import { usePageDeleteModal } from '~/stores/modal';
+import { useSWRxCurrentPage } from '~/stores/page';
 
 
 import BookmarkFolderItem from './BookmarkFolderItem';
 import BookmarkFolderItem from './BookmarkFolderItem';
+import BookmarkItem from './BookmarkItem';
 
 
 import styles from './BookmarkFolderTree.module.scss';
 import styles from './BookmarkFolderTree.module.scss';
 
 
@@ -10,26 +24,135 @@ type BookmarkFolderTreeProps = {
   isUserHomePage?: boolean
   isUserHomePage?: boolean
 }
 }
 
 
+type DragItemDataType = {
+  bookmarkFolder: BookmarkFolderItems
+  level: number
+  parentFolder: BookmarkFolderItems | null
+ } & IPageHasId
+
 const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element => {
 const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element => {
-  const { data: bookmarkFolderData } = useSWRxBookamrkFolderAndChild();
+  const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
+  const { t } = useTranslation();
   const { isUserHomePage } = props;
   const { isUserHomePage } = props;
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: bookmarkFolderData, mutate: mutateBookamrkData } = useSWRxBookamrkFolderAndChild();
+  const { data: userBookmarks, mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
+  const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
+
+  const { open: openDeleteModal } = usePageDeleteModal();
+
+  const onUnbookmarkHandler = useCallback(() => {
+    mutateUserBookmarks();
+    mutateBookmarkInfo();
+  }, [mutateBookmarkInfo, mutateUserBookmarks]);
+
+  const onClickDeleteBookmarkHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
+    const pageDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
+      if (typeof pathOrPathsToDelete !== 'string') {
+        return;
+      }
+      const path = pathOrPathsToDelete;
+
+      if (isCompletely) {
+        toastSuccess(t('deleted_pages_completely', { path }));
+      }
+      else {
+        toastSuccess(t('deleted_pages', { path }));
+      }
+      mutateUserBookmarks();
+      mutateBookmarkInfo();
+      mutateBookamrkData();
+    };
+    openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
+  }, [mutateBookmarkInfo, mutateBookamrkData, mutateUserBookmarks, openDeleteModal, t]);
+
+  const itemDropHandler = async(item: DragItemDataType, dragType: string | null | symbol) => {
+    if (dragType === DRAG_ITEM_TYPE.FOLDER) {
+      try {
+        await apiv3Put('/bookmark-folder', { bookmarkFolderId: item.bookmarkFolder._id, name: item.bookmarkFolder.name, parent: null });
+        await mutateBookamrkData();
+        toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
+      }
+      catch (err) {
+        toastError(err);
+      }
+    }
+    else {
+      try {
+        await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: item._id, folderId: null });
+        await mutateUserBookmarks();
+        toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+      }
+      catch (err) {
+        toastError(err);
+      }
+    }
+
+  };
+  const isDroppable = (item: DragItemDataType, dragType: string | null | symbol) => {
+    if (dragType === DRAG_ITEM_TYPE.FOLDER) {
+      const isRootFolder = item.level === 0;
+      return !isRootFolder;
+    }
+    const isRootBookmark = item.parentFolder == null;
+    return !isRootBookmark;
+
+  };
+
+  const [{ isOver, canDrop }, dropRef] = useDrop(() => ({
+    accept: acceptedTypes,
+    drop: (item: DragItemDataType, monitor) => {
+      const dragType = monitor.getItemType();
+      itemDropHandler(item, dragType);
+    },
+    canDrop: (item: DragItemDataType, monitor) => {
+      const dragType = monitor.getItemType();
+      return isDroppable(item, dragType);
+    },
+    collect: monitor => ({
+      isOver: monitor.isOver({ shallow: true }) && monitor.canDrop(),
+      canDrop: monitor.canDrop(),
+    }),
+  }));
+
 
 
   return (
   return (
     <>
     <>
-      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group px-3 pt-3`}>
-        {bookmarkFolderData?.map((item) => {
-          return (
-            <BookmarkFolderItem
-              key={item._id}
-              bookmarkFolder={item}
-              isOpen={false}
-              level={0}
-              root={item._id}
-              isUserHomePage={isUserHomePage}
-            />
-          );
-        })}
-      </ul>
+      <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}` } >
+        <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group px-2 py-2`}>
+          {bookmarkFolderData?.map((item) => {
+            return (
+              <BookmarkFolderItem
+                key={item._id}
+                bookmarkFolder={item}
+                isOpen={false}
+                level={0}
+                root={item._id}
+                isUserHomePage={isUserHomePage}
+              />
+            );
+          })}
+          {userBookmarks?.map(page => (
+            <div key={page._id} className="grw-foldertree-item-container grw-root-bookmarks">
+              <BookmarkItem
+                bookmarkedPage={page}
+                key={page._id}
+                onUnbookmarked={onUnbookmarkHandler}
+                onRenamed={mutateUserBookmarks}
+                onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
+                parentFolder={null}
+              />
+            </div>
+          ))}
+        </ul>
+        { bookmarkFolderData && bookmarkFolderData.length > 0 && (
+          <div ref={(c) => { dropRef(c) }} className= 'grw-drop-item-area' >
+            { canDrop && isOver && (
+              <div className='grw-accept-drop-item' >{t('bookmark_folder.drop_item_here')}</div>
+            )}
+          </div>
+        ) }
+      </div>
     </>
     </>
   );
   );
 
 

+ 12 - 16
packages/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -10,7 +10,7 @@ import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 import { unbookmark } from '~/client/services/page-operation';
 import { unbookmark } from '~/client/services/page-operation';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { BookmarkFolderItems, DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
 import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxPageInfo } from '~/stores/page';
 import { useSWRxPageInfo } from '~/stores/page';
@@ -25,7 +25,7 @@ type Props = {
   onUnbookmarked: () => void,
   onUnbookmarked: () => void,
   onRenamed: () => void,
   onRenamed: () => void,
   onClickDeleteMenuItem: (pageToDelete: IPageToDeleteWithMeta) => void,
   onClickDeleteMenuItem: (pageToDelete: IPageToDeleteWithMeta) => void,
-  parentFolder: BookmarkFolderItems
+  parentFolder: BookmarkFolderItems | null
 }
 }
 
 
 const BookmarkItem = (props: Props): JSX.Element => {
 const BookmarkItem = (props: Props): JSX.Element => {
@@ -37,18 +37,12 @@ const BookmarkItem = (props: Props): JSX.Element => {
   const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
   const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
   const { latter: pageTitle, former: formerPagePath } = dPagePath;
   const { latter: pageTitle, former: formerPagePath } = dPagePath;
   const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
   const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
-  const [parentId, setParentId] = useState(parentFolder._id);
-  const { mutate: mutateParentBookmarkData } = useSWRxBookamrkFolderAndChild();
-  const { mutate: mutateChildFolderData } = useSWRxBookamrkFolderAndChild(parentId);
-  const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(bookmarkedPage._id);
+  const { mutate: mutateBookamrkData } = useSWRxBookamrkFolderAndChild();
+  const { data: fetchedPageInfo } = useSWRxPageInfo(bookmarkedPage._id);
 
 
   useEffect(() => {
   useEffect(() => {
-    mutatePageInfo();
-    if (parentId != null) {
-      mutateChildFolderData();
-    }
-    mutateParentBookmarkData();
-  }, [parentId, mutateChildFolderData, mutatePageInfo, mutateParentBookmarkData]);
+    mutateBookamrkData();
+  }, [mutateBookamrkData]);
 
 
   const bookmarkMenuItemClickHandler = useCallback(async() => {
   const bookmarkMenuItemClickHandler = useCallback(async() => {
     await unbookmark(bookmarkedPage._id);
     await unbookmark(bookmarkedPage._id);
@@ -112,10 +106,12 @@ const BookmarkItem = (props: Props): JSX.Element => {
   }, [bookmarkedPage, onClickDeleteMenuItem]);
   }, [bookmarkedPage, onClickDeleteMenuItem]);
 
 
   const [, bookmarkItemDragRef] = useDrag({
   const [, bookmarkItemDragRef] = useDrag({
-    type: 'BOOKMARK',
-    item: bookmarkedPage,
-    end: () => {
-      setParentId(parentFolder.parent);
+    type: DRAG_ITEM_TYPE.BOOKMARK,
+    item: { ...bookmarkedPage, parentFolder },
+    end: (_, monitor) => {
+      if (monitor.getDropResult() != null) {
+        mutateBookamrkData();
+      }
     },
     },
     collect: monitor => ({
     collect: monitor => ({
       isDragging: monitor.isDragging(),
       isDragging: monitor.isDragging(),

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

@@ -14,7 +14,7 @@ const BookmarkContents = (): JSX.Element => {
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
-  const { mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(null);
+  const { mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild();
 
 
 
 
   const onClickNewBookmarkFolder = useCallback(() => {
   const onClickNewBookmarkFolder = useCallback(() => {

+ 31 - 16
packages/app/src/components/UsersHomePageFooter.module.scss

@@ -28,26 +28,30 @@ $grw-sidebar-content-footer-height: 50px;
         }
         }
         min-height: 40px;
         min-height: 40px;
         border-radius: 0px;
         border-radius: 0px;
-      }
-      .bookmark-item-list {
-        .picture {
-          width: 16px;
-          height: 16px;
-          vertical-align: text-bottom;
 
 
-          &.picture-md {
-            width: 20px;
-            height: 20px;
+
+        &.bookmark-item-list {
+          .picture {
+            width: 16px;
+            height: 16px;
+            vertical-align: text-bottom;
+
+            &.picture-md {
+              width: 20px;
+              height: 20px;
+            }
+          }
+          svg{
+            width: 14px;
+            height: 14px;
+          }
+          .grw-foldertree-control{
+            margin-left: 1rem;
           }
           }
-        }
-        svg{
-          width: 14px;
-          height: 14px;
         }
         }
       }
       }
-      .grw-foldertree-control{
-        margin-left: 2px;
-      }
+
+
     }
     }
 
 
     .grw-foldertree-item-container {
     .grw-foldertree-item-container {
@@ -79,6 +83,17 @@ $grw-sidebar-content-footer-height: 50px;
         margin-bottom: 3px;
         margin-bottom: 3px;
       }
       }
     }
     }
+    .grw-folder-tree-container {
+      .grw-drop-item-area {
+        min-height: 25vh;
+        padding: 1rem;
+        .grw-accept-drop-item {
+          padding: 0.5rem;
+          border-style: dashed;
+          border-width: 0.15rem;
+        }
+      }
+    }
   }
   }
 }
 }
 
 

+ 3 - 2
packages/app/src/components/UsersHomePageFooter.tsx

@@ -2,8 +2,9 @@ import React, { useCallback, useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { toastError, toastSuccess } from '~/client/util/apiNotification';
+
 import { apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastError, toastSuccess } from '~/client/util/toastr';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentlyCreatedIcon } from '~/components/Icons/RecentlyCreatedIcon';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 import { RecentCreated } from '~/components/RecentCreated/RecentCreated';
 import styles from '~/components/UsersHomePageFooter.module.scss';
 import styles from '~/components/UsersHomePageFooter.module.scss';
@@ -25,7 +26,7 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
   const { creatorId } = props;
   const { creatorId } = props;
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const [isExpanded, setIsExpanded] = useState<boolean>(false);
   const [isExpanded, setIsExpanded] = useState<boolean>(false);
-  const { mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(null);
+  const { mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild();
 
 
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
     try {

+ 7 - 0
packages/app/src/interfaces/bookmark-info.ts

@@ -31,3 +31,10 @@ export interface BookmarkFolderItems {
   children: this[]
   children: this[]
   bookmarks: BookmarkedPage[]
   bookmarks: BookmarkedPage[]
 }
 }
+
+export const DRAG_ITEM_TYPE = {
+  FOLDER: 'FOLDER',
+  BOOKMARK: 'BOOKMARK',
+} as const;
+
+export type DragItemType = typeof DRAG_ITEM_TYPE[keyof typeof DRAG_ITEM_TYPE];

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

@@ -4,13 +4,12 @@ import monggoose, {
 } from 'mongoose';
 } from 'mongoose';
 
 
 
 
-import { IBookmarkFolder, BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { IBookmarkFolder, BookmarkFolderItems, MyBookmarkList } from '~/interfaces/bookmark-info';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 
 
 import loggerFactory from '../../utils/logger';
 import loggerFactory from '../../utils/logger';
 import { getOrCreateModel } from '../util/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 
-import bookmark from './bookmark';
 import { InvalidParentBookmarkFolderError } from './errors';
 import { InvalidParentBookmarkFolderError } from './errors';
 
 
 
 
@@ -22,16 +21,19 @@ export interface BookmarkFolderDocument extends Document {
   _id: Types.ObjectId
   _id: Types.ObjectId
   name: string
   name: string
   owner: Types.ObjectId
   owner: Types.ObjectId
-  parent?: this[]
-  bookmarks?: Types.ObjectId[]
+  parent?: Types.ObjectId | undefined
+  bookmarks?: Types.ObjectId[],
+  children?: BookmarkFolderDocument[]
 }
 }
 
 
 export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
 export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
   createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>
   createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>
   findFolderAndChildren(user: Types.ObjectId | string, parentId?: Types.ObjectId | string): Promise<BookmarkFolderItems[]>
   findFolderAndChildren(user: Types.ObjectId | string, parentId?: Types.ObjectId | string): Promise<BookmarkFolderItems[]>
   deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
   deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
-  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string): Promise<BookmarkFolderDocument>
-  insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string): Promise<BookmarkFolderDocument>
+  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null): Promise<BookmarkFolderDocument>
+  insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise<BookmarkFolderDocument | null>
+  findUserRootBookmarksItem(userId: Types.ObjectId| string): Promise<MyBookmarkList>
+  updateBookmark(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId| string): Promise<BookmarkFolderDocument | null>
 }
 }
 
 
 const bookmarkFolderSchema = new Schema<BookmarkFolderDocument, BookmarkFolderModel>({
 const bookmarkFolderSchema = new Schema<BookmarkFolderDocument, BookmarkFolderModel>({
@@ -83,25 +85,10 @@ bookmarkFolderSchema.statics.findFolderAndChildren = async function(
     userId: Types.ObjectId | string,
     userId: Types.ObjectId | string,
     parentId?: Types.ObjectId | string,
     parentId?: Types.ObjectId | string,
 ): Promise<BookmarkFolderItems[]> {
 ): Promise<BookmarkFolderItems[]> {
+  const folderItems: BookmarkFolderItems[] = [];
 
 
-  let parentFolder: BookmarkFolderDocument | null;
-  let query = {};
-  // Load child bookmark folders
-  if (parentId != null) {
-    parentFolder = await this.findById(parentId);
-    if (parentFolder != null) {
-      query = { owner: userId, parent: parentFolder };
-    }
-    else {
-      throw new InvalidParentBookmarkFolderError('Parent folder not found');
-    }
-  }
-  // Load initial / root bookmark folders
-  else {
-    query = { owner: userId, parent: null };
-  }
-  const bookmarkFolders: BookmarkFolderItems[] = await this.find(query)
-    .populate({ path: 'children' })
+  const folders = await this.find({ owner: userId, parent: parentId })
+    .populate('children')
     .populate({
     .populate({
       path: 'bookmarks',
       path: 'bookmarks',
       model: 'Bookmark',
       model: 'Bookmark',
@@ -110,7 +97,27 @@ bookmarkFolderSchema.statics.findFolderAndChildren = async function(
         model: 'Page',
         model: 'Page',
       },
       },
     });
     });
-  return bookmarkFolders;
+
+  const promises = folders.map(async(folder) => {
+    const children = await this.findFolderAndChildren(userId, folder._id);
+    const {
+      _id, name, owner, bookmarks, parent,
+    } = folder;
+
+    const res = {
+      _id: _id.toString(),
+      name,
+      owner,
+      bookmarks,
+      children,
+      parent,
+    };
+    return res;
+  });
+
+  const results = await Promise.all(promises) as unknown as BookmarkFolderItems[];
+  folderItems.push(...results);
+  return folderItems;
 };
 };
 
 
 bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
 bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
@@ -124,24 +131,29 @@ bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFo
       await Bookmark.deleteMany({ _id: { $in: bookmarks } });
       await Bookmark.deleteMany({ _id: { $in: bookmarks } });
     }
     }
     // Delete all child recursively and update deleted count
     // Delete all child recursively and update deleted count
-    const childFolders = await this.find({ parent: bookmarkFolder });
+    const childFolders = await this.find({ parent: bookmarkFolder._id });
     await Promise.all(childFolders.map(async(child) => {
     await Promise.all(childFolders.map(async(child) => {
       const deletedChildFolder = await this.deleteFolderAndChildren(child._id);
       const deletedChildFolder = await this.deleteFolderAndChildren(child._id);
       deletedCount += deletedChildFolder.deletedCount;
       deletedCount += deletedChildFolder.deletedCount;
     }));
     }));
-    const deletedChild = await this.deleteMany({ parent: bookmarkFolder });
+    const deletedChild = await this.deleteMany({ parent: bookmarkFolder._id });
     deletedCount += deletedChild.deletedCount + 1;
     deletedCount += deletedChild.deletedCount + 1;
     bookmarkFolder.delete();
     bookmarkFolder.delete();
   }
   }
   return { deletedCount };
   return { deletedCount };
 };
 };
 
 
-bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolderId: string, name: string, parentId: string):
+bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolderId: string, name: string, parentId: string | null):
  Promise<BookmarkFolderDocument> {
  Promise<BookmarkFolderDocument> {
-  const parentFolder = await this.findById(parentId);
-  const updateFields = {
-    name, parent: parentFolder?._id || null,
+  const updateFields: {name: string, parent: Types.ObjectId | null} = {
+    name: '',
+    parent: null,
   };
   };
+
+  updateFields.name = name;
+  const parentFolder = parentId ? await this.findById(parentId) : null;
+  updateFields.parent = parentFolder?._id ?? null;
+
   const bookmarkFolder = await this.findByIdAndUpdate(bookmarkFolderId, { $set: updateFields }, { new: true });
   const bookmarkFolder = await this.findByIdAndUpdate(bookmarkFolderId, { $set: updateFields }, { new: true });
   if (bookmarkFolder == null) {
   if (bookmarkFolder == null) {
     throw new Error('Update bookmark folder failed');
     throw new Error('Update bookmark folder failed');
@@ -150,8 +162,8 @@ bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolde
 
 
 };
 };
 
 
-bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string):
-Promise<BookmarkFolderDocument> {
+bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null):
+Promise<BookmarkFolderDocument | null> {
 
 
   // Create bookmark or update existing
   // Create bookmark or update existing
   const bookmarkedPage = await Bookmark.findOneAndUpdate({ page: pageId, user: userId }, { page: pageId, user: userId }, { new: true, upsert: true });
   const bookmarkedPage = await Bookmark.findOneAndUpdate({ page: pageId, user: userId }, { page: pageId, user: userId }, { new: true, upsert: true });
@@ -160,8 +172,46 @@ Promise<BookmarkFolderDocument> {
   await this.updateMany({}, { $pull: { bookmarks:  bookmarkedPage._id } });
   await this.updateMany({}, { $pull: { bookmarks:  bookmarkedPage._id } });
 
 
   // Insert bookmark into bookmark folder
   // Insert bookmark into bookmark folder
-  const bookmarkFolder = await this.findByIdAndUpdate(folderId, { $addToSet: { bookmarks: bookmarkedPage } }, { new: true, upsert: true });
-  return bookmarkFolder;
+  if (folderId != null) {
+    const bookmarkFolder = await this.findByIdAndUpdate(folderId, { $addToSet: { bookmarks: bookmarkedPage } }, { new: true, upsert: true });
+    return bookmarkFolder;
+  }
+
+  return null;
 };
 };
 
 
+bookmarkFolderSchema.statics.findUserRootBookmarksItem = async function(userId: Types.ObjectId | string): Promise<MyBookmarkList> {
+  const bookmarkIdsInFolders = await this.distinct('bookmarks', { owner: userId });
+  const userRootBookmarks: MyBookmarkList = await Bookmark.find({
+    _id: { $nin: bookmarkIdsInFolders },
+  }).populate({
+    path: 'page',
+    model: 'Page',
+    populate: {
+      path: 'lastUpdateUser',
+      model: 'User',
+    },
+  });
+  return userRootBookmarks;
+};
+
+bookmarkFolderSchema.statics.updateBookmark = async function(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId | string):
+Promise<BookmarkFolderDocument | null> {
+  // If isBookmarked
+  if (status) {
+    const bookmarkedPage = await Bookmark.findOne({ page: pageId });
+    const bookmarkFolder = await this.findOne({ owner: userId, bookmarks: { $in: [bookmarkedPage?._id] } });
+    if (bookmarkFolder != null) {
+      await this.updateOne({ owner: userId, _id: bookmarkFolder._id }, { $pull: { bookmarks:  bookmarkedPage?._id } });
+    }
+
+    if (bookmarkedPage) {
+      await bookmarkedPage.delete();
+    }
+    return bookmarkFolder;
+  }
+  // else , Add bookmark
+  await Bookmark.create({ page: pageId, user: userId });
+  return null;
+};
 export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);
 export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);

+ 21 - 12
packages/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -1,6 +1,7 @@
 import { ErrorV3 } from '@growi/core';
 import { ErrorV3 } from '@growi/core';
 import { body } from 'express-validator';
 import { body } from 'express-validator';
 
 
+import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
 import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
@@ -20,7 +21,11 @@ const validator = {
   ],
   ],
   bookmarkPage: [
   bookmarkPage: [
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
     body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
-    body('folderId').isMongoId().withMessage('Folder ID must be a valid mongo ID'),
+    body('folderId').optional({ nullable: true }).isMongoId().withMessage('Folder ID must be a valid mongo ID'),
+  ],
+  bookmark: [
+    body('pageId').isMongoId().withMessage('Page ID must be a valid mongo ID'),
+    body('status').isBoolean().withMessage('status must be one of true or false'),
   ],
   ],
 };
 };
 
 
@@ -50,18 +55,11 @@ module.exports = (crowi) => {
   });
   });
 
 
   // List bookmark folders and child
   // List bookmark folders and child
-  router.get('/list/:parentId?', accessTokenParser, loginRequiredStrictly, async(req, res) => {
-    const { parentId } = req.params;
-    const _parentId = parentId ?? null;
+  router.get('/list', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+
     try {
     try {
-      const bookmarkFolders = await BookmarkFolder.findFolderAndChildren(req.user?._id, _parentId);
-      const bookmarkFolderItems = bookmarkFolders.map(bookmarkFolder => ({
-        _id: bookmarkFolder._id,
-        name: bookmarkFolder.name,
-        parent: bookmarkFolder.parent,
-        children: bookmarkFolder.children,
-        bookmarks: bookmarkFolder.bookmarks,
-      }));
+      const bookmarkFolderItems = await BookmarkFolder.findFolderAndChildren(req.user?._id);
+
       return res.apiv3({ bookmarkFolderItems });
       return res.apiv3({ bookmarkFolderItems });
     }
     }
     catch (err) {
     catch (err) {
@@ -108,5 +106,16 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
+  router.put('/update-bookmark', accessTokenParser, loginRequiredStrictly, validator.bookmark, async(req, res) => {
+    const { pageId, status } = req.body;
+    const userId = req.user?._id;
+    try {
+      const bookmarkFolder = await BookmarkFolder.updateBookmark(pageId, status, userId);
+      return res.apiv3({ bookmarkFolder });
+    }
+    catch (err) {
+      return res.apiv3Err(err, 500);
+    }
+  });
   return router;
   return router;
 };
 };

+ 6 - 30
packages/app/src/server/routes/apiv3/bookmarks.js

@@ -3,7 +3,7 @@ import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
-
+import BookmarkFolder from '../../models/bookmark-folder';
 
 
 const logger = loggerFactory('growi:routes:apiv3:bookmarks'); // eslint-disable-line no-unused-vars
 const logger = loggerFactory('growi:routes:apiv3:bookmarks'); // eslint-disable-line no-unused-vars
 
 
@@ -192,47 +192,23 @@ module.exports = (crowi) => {
    */
    */
   validator.userBookmarkList = [
   validator.userBookmarkList = [
     param('userId').isMongoId().withMessage('userId is required'),
     param('userId').isMongoId().withMessage('userId is required'),
-    query('page').isInt({ min: 1 }),
-    query('limit').if(value => value != null).isInt({ max: 300 }).withMessage('You should set less than 300 or not to set limit.'),
   ];
   ];
 
 
   router.get('/:userId', accessTokenParser, loginRequired, validator.userBookmarkList, apiV3FormValidator, async(req, res) => {
   router.get('/:userId', accessTokenParser, loginRequired, validator.userBookmarkList, apiV3FormValidator, async(req, res) => {
     const { userId } = req.params;
     const { userId } = req.params;
-    const page = req.query.page;
-    const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
 
 
     if (userId == null) {
     if (userId == null) {
       return res.apiv3Err('User id is not found or forbidden', 400);
       return res.apiv3Err('User id is not found or forbidden', 400);
     }
     }
-    if (limit == null) {
-      return res.apiv3Err('Could not catch page limit', 400);
-    }
     try {
     try {
-      const paginationResult = await Bookmark.paginate(
-        {
-          user: { $in: userId },
-        },
-        {
-          populate: {
-            path: 'page',
-            model: 'Page',
-            populate: {
-              path: 'lastUpdateUser',
-              model: 'User',
-            },
-          },
-          page,
-          limit,
-        },
-      );
-
-      paginationResult.docs.forEach((doc) => {
-        if (doc.page.lastUpdateUser != null && doc.page.lastUpdateUser instanceof User) {
-          doc.page.lastUpdateUser = serializeUserSecurely(doc.page.lastUpdateUser);
+      const userRootBookmarks = await BookmarkFolder.findUserRootBookmarksItem(userId);
+      userRootBookmarks.forEach((bookmark) => {
+        if (bookmark.page.lastUpdateUser != null && bookmark.page.lastUpdateUser instanceof User) {
+          bookmark.page.lastUpdateUser = serializeUserSecurely(bookmark.page.lastUpdateUser);
         }
         }
       });
       });
 
 
-      return res.apiv3({ paginationResult });
+      return res.apiv3({ userRootBookmarks });
     }
     }
     catch (err) {
     catch (err) {
       logger.error('get-bookmark-failed', err);
       logger.error('get-bookmark-failed', err);

+ 3 - 4
packages/app/src/stores/bookmark-folder.ts

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

+ 5 - 6
packages/app/src/stores/bookmark.ts

@@ -1,4 +1,4 @@
-import { IUserHasId, Nullable } from '@growi/core';
+import { IUserHasId } from '@growi/core';
 import { SWRResponse } from 'swr';
 import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
@@ -22,15 +22,14 @@ export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRRespon
   );
   );
 };
 };
 
 
-export const useSWRxCurrentUserBookmarks = (pageNum?: Nullable<number>): SWRResponse<IPageHasId[], Error> => {
+export const useSWRxCurrentUserBookmarks = (): SWRResponse<IPageHasId[], Error> => {
   const { data: currentUser } = useCurrentUser();
   const { data: currentUser } = useCurrentUser();
-  const currentPage = pageNum ?? 1;
   const user = currentUser as IUserHasId;
   const user = currentUser as IUserHasId;
   return useSWRImmutable(
   return useSWRImmutable(
     currentUser != null ? `/bookmarks/${user._id}` : null,
     currentUser != null ? `/bookmarks/${user._id}` : null,
-    endpoint => apiv3Get(endpoint, { page: currentPage }).then((response) => {
-      const { paginationResult } = response.data;
-      return paginationResult.docs.map((item) => {
+    endpoint => apiv3Get(endpoint).then((response) => {
+      const { userRootBookmarks } = response.data;
+      return userRootBookmarks.map((item) => {
         return {
         return {
           ...item.page,
           ...item.page,
         };
         };

+ 4 - 0
packages/app/src/styles/molecules/_bookmark-folder-tree.scss

@@ -72,6 +72,10 @@ $grw-bookmark-item-padding-left: 45px;
     > .grw-foldertree-item-container {
     > .grw-foldertree-item-container {
       > .list-group-item {
       > .list-group-item {
         padding-left: 0;
         padding-left: 0;
+      } &.grw-root-bookmarks{
+        .list-group-item.bookmark-item-list {
+          padding-left: $grw-foldertree-item-padding-left + 25;
+        }
       }
       }
       > .list-group-item.bookmark-item-list {
       > .list-group-item.bookmark-item-list {
         padding-left: $grw-bookmark-item-padding-left;
         padding-left: $grw-bookmark-item-padding-left;

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

@@ -370,6 +370,14 @@
     }
     }
 
 
     // bookmarks
     // bookmarks
+    .grw-folder-tree-container {
+      .grw-drop-item-area , .grw-foldertree-item-container {
+        .grw-accept-drop-item {
+          background-color: hsl.lighten(var(--bgcolor-sidebar-context), 10%) !important;
+          border-color: hsl.lighten(var(--bgcolor-sidebar-context), 30%) !important;
+        }
+      }
+    }
     .grw-bookmarks-list {
     .grw-bookmarks-list {
       @include override-list-group-item-for-pagetree(
       @include override-list-group-item-for-pagetree(
         var(--color-sidebar-context),
         var(--color-sidebar-context),
@@ -429,6 +437,14 @@
         hsl.lighten($body-bg, 24%)
         hsl.lighten($body-bg, 24%)
       );
       );
     }
     }
+    .grw-folder-tree-container {
+      .grw-drop-item-area , .grw-foldertree-item-container {
+        .grw-accept-drop-item {
+          background-color: hsl.lighten(var($body-bg), 10%) !important;
+          border-color: hsl.lighten(var($body-bg), 30%) !important;
+        }
+      }
+    }
   }
   }
 
 
   // Bookmark dropdown menu
   // Bookmark dropdown menu

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

@@ -238,6 +238,15 @@
     }
     }
 
 
     // bookmark
     // bookmark
+    .grw-folder-tree-container {
+      .grw-drop-item-area, .grw-foldertree-item-container  {
+        .grw-accept-drop-item {
+          background-color: hsl.darken(var(--bgcolor-sidebar-context), 10%) !important;
+          border-color: hsl.darken(var(--bgcolor-sidebar-context), 30%) !important;
+        }
+      }
+    }
+
     .grw-bookmarks-list {
     .grw-bookmarks-list {
       @include override-list-group-item-for-pagetree(
       @include override-list-group-item-for-pagetree(
         var(--color-sidebar-context),
         var(--color-sidebar-context),
@@ -300,6 +309,14 @@
         hsl.darken($body-bg, 24%)
         hsl.darken($body-bg, 24%)
       );
       );
     }
     }
+    .grw-folder-tree-container {
+      .grw-drop-item-area, .grw-foldertree-item-container  {
+        .grw-accept-drop-item {
+          background-color: hsl.darken(var($body-bg), 10%) !important;
+          border-color: hsl.darken(var($body-bg), 30%) !important;
+        }
+      }
+    }
   }
   }
 
 
   // Bookmark dropdown menu
   // Bookmark dropdown menu