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

Add / remove root bookmark from sub-navigation

https://youtrack.weseek.co.jp/issue/GW-7920
- Add method to update bookmark in bookmark-folder model
- Add /update-bookmark route and validation rules
- Simplify renderBookmarkMenuItem mehod
- Add drop down item to show add/remove root bookmark
- Add method to toggle between add and remove bookmark
- Adjust onMenuItemClickHandler and onClickChildMenuItemHandler to handle existing bookmark
- Update translation for bookmark/unbookamrk dropdown item
Mudana-Grune 3 лет назад
Родитель
Сommit
4f50098894

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

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

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

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

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

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

+ 118 - 56
packages/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -1,5 +1,5 @@
 import React, {
-  useCallback, useState,
+  useCallback, useEffect, useState,
 } from 'react';
 
 import { useTranslation } from 'next-i18next';
@@ -7,9 +7,10 @@ import {
   DropdownItem, DropdownMenu, UncontrolledDropdown,
 } 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 { useSWRBookmarkInfo } from '~/stores/bookmark';
+import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxCurrentPage } from '~/stores/page';
 
@@ -28,32 +29,82 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
   const { children } = props;
   const [isCreateAction, setIsCreateAction] = useState(false);
   const { data: bookmarkFolders, mutate: mutateBookmarkFolderData } = useSWRxBookamrkFolderAndChild();
+  const [updateParent, setUpdateParent] = useState<string|null>(null);
+  const { mutate: mutateChildBookmarkFolderData } = useSWRxBookamrkFolderAndChild(updateParent);
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   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 [bookmarkData, setBookmarkData] = useState<BookmarkFolderItems[]>([]);
+
+
+  useEffect(() => {
+    if (updateParent) {
+      mutateBookmarkFolderData();
+      mutateChildBookmarkFolderData();
+    }
+  }, [mutateBookmarkFolderData, mutateChildBookmarkFolderData, updateParent]);
+
+  useEffect(() => {
+    if (!isOpen) {
+      setBookmarkData([]);
+    }
+    else if (bookmarkFolders != null) {
+      setBookmarkData(bookmarkFolders);
+    }
+  }, [bookmarkFolders, isOpen]);
+
+  const toggleBookmarkHandler = useCallback(async() => {
+
+    try {
+      const res = await apiv3Put('/bookmark-folder/update-bookmark', { pageId: currentPage?._id, status: isBookmarked });
+      const { bookmarkFolder } = res.data;
+      if (bookmarkFolder != null) {
+        setUpdateParent(bookmarkFolder.parent);
+      }
+      toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+    mutateUserBookmarks();
+    mutateBookmarkInfo();
+    setSelectedItem(null);
+    mutateBookmarkFolderData();
+  }, [currentPage, isBookmarked, mutateBookmarkFolderData, mutateBookmarkInfo, mutateUserBookmarks, t]);
 
 
   const onClickNewBookmarkFolder = useCallback(() => {
     setIsCreateAction(true);
   }, []);
 
+
   const toggleHandler = useCallback(() => {
+    setIsOpen(!isOpen);
     mutateBookmarkFolderData();
-    bookmarkFolders?.forEach((bookmarkFolder) => {
-      bookmarkFolder.bookmarks.forEach((bookmark) => {
-        if (bookmark.page._id === currentPage?._id) {
-          setSelectedItem(bookmarkFolder._id);
-        }
+    if (isOpen && bookmarkData != null) {
+      bookmarkData?.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]);
+    }
+  }, [bookmarkData, currentPage?._id, isOpen, mutateBookmarkFolderData]);
+
 
   const isBookmarkFolderExists = useCallback((): boolean => {
-    if (bookmarkFolders && bookmarkFolders.length > 0) {
+    if (bookmarkData && bookmarkData.length > 0) {
       return true;
     }
     return false;
-  }, [bookmarkFolders]);
+  }, [bookmarkData]);
 
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
@@ -70,10 +121,17 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
 
   const onMenuItemClickHandler = useCallback(async(itemId: string) => {
     try {
-      await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: itemId });
-
-      mutateBookmarkInfo();
-      toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+      // Remove from root folder then move to selected folder
+      if (isBookmarked) {
+        await toggleBookmarkHandler();
+        await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: itemId });
+      }
+      // Move to selected folder
+      else {
+        await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: itemId });
+        mutateBookmarkInfo();
+        toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+      }
     }
     catch (err) {
       toastError(err);
@@ -81,53 +139,57 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
 
     mutateBookmarkFolderData();
     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>
+  }, [currentPage?._id, isBookmarked, mutateBookmarkFolderData, mutateBookmarkInfo, t, toggleBookmarkHandler]);
+
+
+  const renderBookmarkMenuItem = (child ?:BookmarkFolderItems[]) => {
+    if (!child) {
+      return (
+        <>
+          <DropdownItem toggle={false} onClick={toggleBookmarkHandler} className={`grw-bookmark-folder-menu-item ${isBookmarked ? 'text-danger' : ''}`}>
+            <i className="fa fa-bookmark"></i> <span className="mx-2 ">
+              { isBookmarked ? t('bookmark_folder.cancel_bookmark') : t('bookmark_folder.bookmark_this_page')}
+            </span>
           </DropdownItem>
-        )}
-        { isBookmarkFolderExists() && (
-          <>
-            <DropdownItem divider />
-            {bookmarkFolders?.map(folder => (
-              <div key={folder._id} >
-                {
+          <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 />
+              {bookmarkData?.map(folder => (
+                <div key={folder._id}>
                   <div className='dropdown-item grw-bookmark-folder-menu-item' tabIndex={0} role="menuitem" onClick={() => onMenuItemClickHandler(folder._id)}>
                     <BookmarkFolderMenuItem
                       item={folder}
                       isSelected={selectedItem === folder._id}
                       onSelectedChild={() => setSelectedItem(null)}
                     />
+                    {isOpen && (
+                      <div className="bookmark-folder-submenu">
+                        {renderBookmarkMenuItem(folder.children)}
+                      </div>
+                    )}
                   </div>
-                }
-              </div>
-            ))}
-          </>
-        )}
-      </>
-    );
-  }, [bookmarkFolders,
-      isBookmarkFolderExists,
-      isCreateAction,
-      onClickNewBookmarkFolder,
-      onMenuItemClickHandler,
-      onPressEnterHandlerForCreate,
-      t,
-      selectedItem,
-  ]);
+                </div>
+              ))}
+            </>
+          )}
+        </>
+      );
+    }
+  };
 
   return (
     <UncontrolledDropdown

+ 20 - 7
packages/app/src/components/Bookmarks/BookmarkFolderMenuItem.tsx

@@ -6,11 +6,12 @@ import {
   DropdownMenu, DropdownToggle, UncontrolledDropdown, UncontrolledTooltip,
 } from 'reactstrap';
 
-import { apiv3Post } from '~/client/util/apiv3-client';
+import { unbookmark } from '~/client/services/page-operation';
+import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { onDeletedBookmarkFolderFunction } from '~/interfaces/ui';
-import { useSWRBookmarkInfo } from '~/stores/bookmark';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 import { useSWRxCurrentPage } from '~/stores/page';
@@ -38,8 +39,11 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
   const [selectedItem, setSelectedItem] = useState<string | null>(null);
   const [isCreateAction, setIsCreateAction] = useState<boolean>(false);
   const { data: currentPage } = useSWRxCurrentPage();
-  const { mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
+  const { data: userBookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
+  const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
+
+  const isBookmarked = userBookmarkInfo?.isBookmarked;
 
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
@@ -104,19 +108,28 @@ const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
 
   const onClickChildMenuItemHandler = useCallback(async(e, item) => {
     e.stopPropagation();
-    mutateBookmarkInfo();
     onSelectedChild();
     try {
-      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' }));
+      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 });
+        mutateUserBookmarks();
+      }
+      else {
+        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' }));
+        mutateUserBookmarks();
+      }
+
       mutateParentFolders();
       mutateChildFolders();
       setSelectedItem(item._id);
+      mutateBookmarkInfo();
     }
     catch (err) {
       toastError(err);
     }
-  }, [mutateBookmarkInfo, onSelectedChild, currentPage?._id, mutateParentFolders, mutateChildFolders, t]);
+  }, [mutateBookmarkInfo, onSelectedChild, isBookmarked, mutateChildFolders, currentPage?._id, mutateUserBookmarks, t, mutateParentFolders]);
 
   const renderBookmarkSubMenuItem = useCallback(() => {
     return (

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

@@ -32,6 +32,7 @@ export interface BookmarkFolderModel extends Model<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>({
@@ -188,4 +189,23 @@ bookmarkFolderSchema.statics.findUserRootBookmarksItem = async function(userId:
   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);

+ 15 - 0
packages/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -22,6 +22,10 @@ const validator = {
     body('pageId').isMongoId().withMessage('Page 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'),
+  ],
 };
 
 module.exports = (crowi) => {
@@ -108,5 +112,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;
 };