Преглед изворни кода

Merge pull request #7345 from weseek/feat/gw-7913-implement-drag-and-drop-to-bookmark-folder

feat: gw 7913 implement drag and drop to bookmark folder
Ryoji Shimizu пре 3 година
родитељ
комит
3e2b396abb

+ 2 - 1
packages/app/public/static/locales/en_US/commons.json

@@ -16,7 +16,8 @@
     "update_failed": "Failed to update {{target}}",
     "update_failed": "Failed to update {{target}}",
     "delete_succeeded": "Succeeded to delete {{target}}",
     "delete_succeeded": "Succeeded to delete {{target}}",
     "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
     "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
-    "remove_share_link": "Succeeded to remove {{count}} share links"
+    "remove_share_link": "Succeeded to remove {{count}} share links",
+    "add_succeeded": "Succeeded to add {{target}}"
   },
   },
   "alert": {
   "alert": {
     "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",

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

@@ -815,6 +815,7 @@
   },
   },
   "bookmark_folder":{
   "bookmark_folder":{
     "bookmark_folder": "bookmark folder",
     "bookmark_folder": "bookmark folder",
+    "bookmark": "bookmark",
     "delete_modal": {
     "delete_modal": {
       "modal_header_label": "Delete Bookmark Folder",
       "modal_header_label": "Delete Bookmark Folder",
       "modal_body_description": "Delete this bookmark folder and its contents",
       "modal_body_description": "Delete this bookmark folder and its contents",

+ 2 - 1
packages/app/public/static/locales/ja_JP/commons.json

@@ -16,7 +16,8 @@
     "update_failed": "{{target}}の更新に失敗しました",
     "update_failed": "{{target}}の更新に失敗しました",
     "delete_succeeded": "{{target}} の削除に成功しました",
     "delete_succeeded": "{{target}} の削除に成功しました",
     "remove_share_link_success": "{{shareLinkId}}を削除しました",
     "remove_share_link_success": "{{shareLinkId}}を削除しました",
-    "remove_share_link": "共有リンクを{{count}}件削除しました"
+    "remove_share_link": "共有リンクを{{count}}件削除しました",
+    "add_succeeded": "新しい{{target}}が追加されました"
   },
   },
   "alert": {
   "alert": {
     "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",

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

@@ -815,6 +815,7 @@
   },
   },
   "bookmark_folder":{
   "bookmark_folder":{
     "bookmark_folder": "ブックマークフォルダ",
     "bookmark_folder": "ブックマークフォルダ",
+    "bookmark": "ブックマーク",
     "delete_modal": {
     "delete_modal": {
       "modal_header_label": "ブックマークフォルダを削除",
       "modal_header_label": "ブックマークフォルダを削除",
       "modal_body_description": "このブックマーク フォルダとその内容を削除する",
       "modal_body_description": "このブックマーク フォルダとその内容を削除する",

+ 2 - 1
packages/app/public/static/locales/zh_CN/commons.json

@@ -16,7 +16,8 @@
     "update_failed": "Failed to update {{target}}",
     "update_failed": "Failed to update {{target}}",
     "delete_succeeded": "Succeeded to delete {{target}}",
     "delete_succeeded": "Succeeded to delete {{target}}",
     "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
     "remove_share_link_success": "Succeeded to remove {{shareLinkId}}",
-    "remove_share_link": "Succeeded to remove {{count}} share links"
+    "remove_share_link": "Succeeded to remove {{count}} share links",
+    "add_succeeded": "Succeeded to add {{target}}"
   },
   },
   "alert": {
   "alert": {
     "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",
     "siteUrl_is_not_set": "主页URL未设置,通过 {{link}} 设置",

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

@@ -818,6 +818,7 @@
   },
   },
   "bookmark_folder":{
   "bookmark_folder":{
     "bookmark_folder": "书签文件夹",
     "bookmark_folder": "书签文件夹",
+    "bookmark": "书签",
     "delete_modal": {
     "delete_modal": {
       "modal_header_label": "删除书签文件夹",
       "modal_header_label": "删除书签文件夹",
       "modal_body_description": "删除此书签文件夹及其内容",
       "modal_body_description": "删除此书签文件夹及其内容",

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

@@ -3,6 +3,7 @@ import {
 } from 'react';
 } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
+import { useDrag, useDrop } from 'react-dnd';
 import { DropdownToggle } from 'reactstrap';
 import { DropdownToggle } from 'reactstrap';
 
 
 import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Delete, apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
@@ -11,7 +12,7 @@ import CountBadge from '~/components/Common/CountBadge';
 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 } from '~/interfaces/bookmark-info';
-import { IPageToDeleteWithMeta } from '~/interfaces/page';
+import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { useSWRBookmarkInfo } from '~/stores/bookmark';
 import { useSWRBookmarkInfo } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
@@ -27,9 +28,13 @@ import DeleteBookmarkFolderModal from './DeleteBookmarkFolderModal';
 type BookmarkFolderItemProps = {
 type BookmarkFolderItemProps = {
   bookmarkFolder: BookmarkFolderItems
   bookmarkFolder: BookmarkFolderItems
   isOpen?: boolean
   isOpen?: boolean
+  level: number
+  root: string
 }
 }
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
-  const { bookmarkFolder, isOpen: _isOpen = false } = props;
+  const {
+    bookmarkFolder, isOpen: _isOpen = false, level, root,
+  } = props;
 
 
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
@@ -139,7 +144,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
   }, [hasChildren, isOpen]);
   }, [hasChildren, isOpen]);
 
 
   const onClickDeleteBookmarkHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
   const onClickDeleteBookmarkHandler = useCallback((pageToDelete: IPageToDeleteWithMeta) => {
-    const pageDeletedHandler : OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
+    const pageDeletedHandler: OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
       if (typeof pathOrPathsToDelete !== 'string') {
       if (typeof pathOrPathsToDelete !== 'string') {
         return;
         return;
       }
       }
@@ -162,6 +167,73 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
     mutateBookmarkInfo();
     mutateBookmarkInfo();
   }, [mutateBookmarkInfo, mutateParentBookmarkFolder]);
   }, [mutateBookmarkInfo, mutateParentBookmarkFolder]);
 
 
+  const [, bookmarkFolderDragRef] = useDrag({
+    type: 'FOLDER',
+    item: props,
+    end: (item, monitor) => {
+      const dropResult = monitor.getDropResult();
+      if (dropResult != null) {
+        mutateParentBookmarkFolder();
+      }
+    },
+    collect: monitor => ({
+      isDragging: monitor.isDragging(),
+      canDrag: monitor.canDrag(),
+    }),
+  });
+
+
+  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' }));
+    }
+    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);
+    }
+
+  }, [bookmarkFolder._id, mutateParentBookmarkFolder, t]);
+
+
+  const isDroppable = (item: BookmarkFolderItemProps, targetRoot: string, targetLevel: number): boolean => {
+    if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
+      return false;
+    }
+    return item.root !== targetRoot || item.level >= targetLevel;
+  };
+
+  const [, bookmarkFolderDropRef] = useDrop(() => ({
+    accept: 'FOLDER',
+    drop: folderItemDropHandler,
+    canDrop: (item) => {
+      // Implement isDropable function & improve
+      return isDroppable(item, root, level);
+    },
+    collect: monitor => ({
+      isOver: monitor.isOver(),
+    }),
+  }));
+
+  const [, bookmarkItemDropRef] = useDrop(() => ({
+    accept: 'BOOKMARK',
+    drop: bookmarkItemDropHandler,
+    collect: monitor => ({
+      isOver: monitor.isOver(),
+    }),
+  }));
+
   const renderChildFolder = () => {
   const renderChildFolder = () => {
     return isOpen && currentChildren?.map((childFolder) => {
     return isOpen && currentChildren?.map((childFolder) => {
       return (
       return (
@@ -169,6 +241,8 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
           <BookmarkFolderItem
           <BookmarkFolderItem
             key={childFolder._id}
             key={childFolder._id}
             bookmarkFolder={childFolder}
             bookmarkFolder={childFolder}
+            level={level + 1}
+            root={root}
           />
           />
         </div>
         </div>
       );
       );
@@ -184,6 +258,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
           onUnbookmarked={onUnbookmarkHandler}
           onUnbookmarked={onUnbookmarkHandler}
           onRenamed={mutateParentBookmarkFolder}
           onRenamed={mutateParentBookmarkFolder}
           onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
           onClickDeleteMenuItem={onClickDeleteBookmarkHandler}
+          parentFolder={bookmarkFolder}
         />
         />
       );
       );
     });
     });
@@ -204,7 +279,10 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
 
 
   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 className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center" onClick={loadChildFolder}>
+      <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"
+        onClick={loadChildFolder}
+      >
         <div className="grw-triangle-container d-flex justify-content-center">
         <div className="grw-triangle-container d-flex justify-content-center">
           {hasChildren() && (
           {hasChildren() && (
             <button
             <button
@@ -223,7 +301,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
             <FolderIcon isOpen={isOpen} />
             <FolderIcon isOpen={isOpen} />
           </div>
           </div>
         }
         }
-        { isRenameAction ? (
+        {isRenameAction ? (
           <BookmarkFolderNameInput
           <BookmarkFolderNameInput
             onClickOutside={() => setIsRenameAction(false)}
             onClickOutside={() => setIsRenameAction(false)}
             onPressEnter={onPressEnterHandlerForRename}
             onPressEnter={onPressEnterHandlerForRename}
@@ -236,7 +314,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
             </div>
             </div>
             {hasChildren() && (
             {hasChildren() && (
               <div className="grw-foldertree-count-wrapper">
               <div className="grw-foldertree-count-wrapper">
-                <CountBadge count={ childCount } />
+                <CountBadge count={childCount} />
               </div>
               </div>
             )}
             )}
           </>
           </>
@@ -283,7 +361,7 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
         bookmarkFolder={bookmarkFolder}
         bookmarkFolder={bookmarkFolder}
         isOpen={isDeleteFolderModalShown}
         isOpen={isDeleteFolderModalShown}
         onClickDeleteButton={onClickDeleteButtonHandler}
         onClickDeleteButton={onClickDeleteButtonHandler}
-        onModalClose={onDeleteFolderModalClose}/>
+        onModalClose={onDeleteFolderModalClose} />
     </div>
     </div>
   );
   );
 };
 };

+ 3 - 3
packages/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -60,7 +60,7 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
       await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
       await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
       await mutateBookmarkFolderData();
       await mutateBookmarkFolderData();
       setIsCreateAction(false);
       setIsCreateAction(false);
-      toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder') }));
+      toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -73,7 +73,7 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
       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 });
 
 
       mutateBookmarkInfo();
       mutateBookmarkInfo();
-      toastSuccess('Bookmark added to bookmark folder successfully');
+      toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -81,7 +81,7 @@ const BookmarkFolderMenu = (props: Props): JSX.Element => {
 
 
     mutateBookmarkFolderData();
     mutateBookmarkFolderData();
     setSelectedItem(itemId);
     setSelectedItem(itemId);
-  }, [currentPage?._id, mutateBookmarkFolderData, mutateBookmarkInfo]);
+  }, [currentPage?._id, mutateBookmarkFolderData, mutateBookmarkInfo, t]);
 
 
   const renderBookmarkMenuItem = useCallback(() => {
   const renderBookmarkMenuItem = useCallback(() => {
     return (
     return (

+ 3 - 3
packages/app/src/components/Bookmarks/BookmarkFolderMenuItem.tsx

@@ -43,7 +43,7 @@ const BookmarkFolderMenuItem = (props: Props):JSX.Element => {
       await apiv3Post('/bookmark-folder', { name: folderName, parent: item._id });
       await apiv3Post('/bookmark-folder', { name: folderName, parent: item._id });
       await mutateChildFolders();
       await mutateChildFolders();
       setIsCreateAction(false);
       setIsCreateAction(false);
-      toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder') }));
+      toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -91,7 +91,7 @@ const BookmarkFolderMenuItem = (props: Props):JSX.Element => {
     onSelectedChild();
     onSelectedChild();
     try {
     try {
       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('Bookmark added to bookmark folder successfully');
+      toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
       mutateParentFolders();
       mutateParentFolders();
       mutateChildFolders();
       mutateChildFolders();
       setSelectedItem(item._id);
       setSelectedItem(item._id);
@@ -101,7 +101,7 @@ const BookmarkFolderMenuItem = (props: Props):JSX.Element => {
     }
     }
 
 
 
 
-  }, [mutateBookmarkInfo, onSelectedChild, currentPage?._id, mutateParentFolders, mutateChildFolders]);
+  }, [mutateBookmarkInfo, onSelectedChild, currentPage?._id, mutateParentFolders, mutateChildFolders, t]);
 
 
   const renderBookmarkSubMenuItem = useCallback(() => {
   const renderBookmarkSubMenuItem = useCallback(() => {
     return (
     return (

+ 2 - 0
packages/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -18,6 +18,8 @@ const BookmarkFolderTree = (): JSX.Element => {
               key={item._id}
               key={item._id}
               bookmarkFolder={item}
               bookmarkFolder={item}
               isOpen={false}
               isOpen={false}
+              level={0}
+              root={item._id}
             />
             />
           );
           );
         })}
         })}

+ 76 - 42
packages/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -1,16 +1,18 @@
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useEffect, useState } from 'react';
 
 
 import nodePath from 'path';
 import nodePath from 'path';
 
 
 import { DevidedPagePath, pathUtils } from '@growi/core';
 import { DevidedPagePath, pathUtils } from '@growi/core';
+import { useDrag } from 'react-dnd';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 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 { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
-
+import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 
 
 import ClosableTextInput, { AlertInfo, AlertType } from '../Common/ClosableTextInput';
 import ClosableTextInput, { AlertInfo, AlertType } from '../Common/ClosableTextInput';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
@@ -20,18 +22,29 @@ type Props = {
   bookmarkedPage: IPageHasId,
   bookmarkedPage: IPageHasId,
   onUnbookmarked: () => void,
   onUnbookmarked: () => void,
   onRenamed: () => void,
   onRenamed: () => void,
-  onClickDeleteMenuItem: (pageToDelete: IPageToDeleteWithMeta) => void
+  onClickDeleteMenuItem: (pageToDelete: IPageToDeleteWithMeta) => void,
+  parentFolder: BookmarkFolderItems
 }
 }
 
 
 const BookmarkItem = (props: Props): JSX.Element => {
 const BookmarkItem = (props: Props): JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
   const {
   const {
-    bookmarkedPage, onUnbookmarked, onRenamed, onClickDeleteMenuItem,
+    bookmarkedPage, onUnbookmarked, onRenamed, onClickDeleteMenuItem, parentFolder,
   } = props;
   } = props;
   const [isRenameInputShown, setRenameInputShown] = useState(false);
   const [isRenameInputShown, setRenameInputShown] = useState(false);
   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);
+
+
+  useEffect(() => {
+    if (parentId != null) {
+      mutateChildFolderData();
+    }
+  }, [parentId, mutateChildFolderData]);
 
 
   const bookmarkMenuItemClickHandler = useCallback(async() => {
   const bookmarkMenuItemClickHandler = useCallback(async() => {
     await unbookmark(bookmarkedPage._id);
     await unbookmark(bookmarkedPage._id);
@@ -94,45 +107,66 @@ const BookmarkItem = (props: Props): JSX.Element => {
     onClickDeleteMenuItem(pageToDelete);
     onClickDeleteMenuItem(pageToDelete);
   }, [bookmarkedPage, onClickDeleteMenuItem]);
   }, [bookmarkedPage, onClickDeleteMenuItem]);
 
 
+  const [, bookmarkItemDragRef] = useDrag({
+    type: 'BOOKMARK',
+    item: bookmarkedPage,
+    end: () => {
+      if (parentFolder.parent == null) {
+        mutateParentBookmarkData();
+      }
+      if (parentId != null) {
+        setParentId(parentFolder.parent);
+        mutateChildFolderData();
+      }
+    },
+    collect: monitor => ({
+      isDragging: monitor.isDragging(),
+      canDrag: monitor.canDrag(),
+    }),
+  });
+
+
   return (
   return (
-    <div className="grw-foldertree-item-container" key={bookmarkedPage._id}>
-      <li className="bookmark-item-list list-group-item list-group-item-action border-0 py-0 pl-5 d-flex align-items-center" id={bookmarkItemId}>
-        { isRenameInputShown ? (
-          <ClosableTextInput
-            value={nodePath.basename(bookmarkedPage.path ?? '')}
-            placeholder={t('Input page name')}
-            onClickOutside={() => { setRenameInputShown(false) }}
-            onPressEnter={pressEnterForRenameHandler}
-            inputValidator={inputValidator}
-          />
-        ) : (
-          <a href={`/${bookmarkedPage._id}`} className="grw-foldertree-title-anchor flex-grow-1 pr-3">
-            <p className={`text-truncate m-auto ${bookmarkedPage.isEmpty && 'grw-sidebar-text-muted'}`}>{pageTitle}</p>
-          </a>
-        )}
-        <PageItemControl
-          pageId={bookmarkedPage._id}
-          isEnableActions
-          forceHideMenuItems={[MenuItemType.DUPLICATE]}
-          onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
-          onClickRenameMenuItem={renameMenuItemClickHandler}
-          onClickDeleteMenuItem={deleteMenuItemClickHandler}
-        >
-          <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
-            <i className="icon-options fa fa-rotate-90 p-1"></i>
-          </DropdownToggle>
-        </PageItemControl>
-        <UncontrolledTooltip
-          modifiers={{ preventOverflow: { boundariesElement: 'window' } }}
-          autohide={false}
-          placement="right"
-          target={bookmarkItemId}
-          fade={false}
-        >
-          { formerPagePath !== null ? `${formerPagePath}/` : '/' }
-        </UncontrolledTooltip>
-      </li>
-    </div>
+    <li
+      className="bookmark-item-list list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center"
+      key={bookmarkedPage._id} ref={(c) => { bookmarkItemDragRef(c) }}
+      id={bookmarkItemId}
+    >
+      {isRenameInputShown ? (
+        <ClosableTextInput
+          value={nodePath.basename(bookmarkedPage.path ?? '')}
+          placeholder={t('Input page name')}
+          onClickOutside={() => { setRenameInputShown(false) }}
+          onPressEnter={pressEnterForRenameHandler}
+          inputValidator={inputValidator}
+        />
+      ) : (
+        <a href={`/${bookmarkedPage._id}`} className="grw-foldertree-title-anchor flex-grow-1 pr-3">
+          <p className={`text-truncate m-auto ${bookmarkedPage.isEmpty && 'grw-sidebar-text-muted'}`}>{pageTitle}</p>
+        </a>
+      )}
+      <PageItemControl
+        pageId={bookmarkedPage._id}
+        isEnableActions
+        forceHideMenuItems={[MenuItemType.DUPLICATE]}
+        onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
+        onClickRenameMenuItem={renameMenuItemClickHandler}
+        onClickDeleteMenuItem={deleteMenuItemClickHandler}
+      >
+        <DropdownToggle color="transparent" className="border-0 rounded btn-page-item-control p-0 grw-visible-on-hover mr-1">
+          <i className="icon-options fa fa-rotate-90 p-1"></i>
+        </DropdownToggle>
+      </PageItemControl>
+      <UncontrolledTooltip
+        modifiers={{ preventOverflow: { boundariesElement: 'window' } }}
+        autohide={false}
+        placement="right"
+        target={bookmarkItemId}
+        fade={false}
+      >
+        {formerPagePath !== null ? `${formerPagePath}/` : '/'}
+      </UncontrolledTooltip>
+    </li>
   );
   );
 };
 };
 
 

+ 0 - 65
packages/app/src/components/Bookmarks/BookmarkItemList.tsx

@@ -1,65 +0,0 @@
-import React, { useCallback } from 'react';
-
-import { useTranslation } from 'next-i18next';
-
-import { toastSuccess } from '~/client/util/toastr';
-import { IPageToDeleteWithMeta } from '~/interfaces/page';
-import { OnDeletedFunction } from '~/interfaces/ui';
-import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
-import { usePageDeleteModal } from '~/stores/modal';
-
-import BookmarkItem from './BookmarkItem';
-
-import styles from './BookmarkFolderTree.module.scss';
-
-
-const BookmarkItemList = (): JSX.Element => {
-  const { t } = useTranslation();
-  const { data: currentUserBookmarksData, mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
-  const { open: openDeleteModal } = usePageDeleteModal();
-
-  const deleteMenuItemClickHandler = 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 }));
-      }
-      mutateCurrentUserBookmarks();
-    };
-    openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
-  }, [mutateCurrentUserBookmarks, openDeleteModal, t]);
-
-  return (
-    <>
-      {currentUserBookmarksData?.length === 0 && (
-        <div className="pt-3">
-          <h5 className="pl-3">
-            { t('No bookmarks yet') }
-          </h5>
-        </div>
-      )}
-      <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group px-3 pt-2 pb-3`}>
-        { currentUserBookmarksData?.map((currentUserBookmark) => {
-          return (
-            <BookmarkItem
-              key={currentUserBookmark._id}
-              bookmarkedPage={currentUserBookmark}
-              onUnbookmarked={mutateCurrentUserBookmarks}
-              onRenamed={mutateCurrentUserBookmarks}
-              onClickDeleteMenuItem={deleteMenuItemClickHandler}
-            />
-          );
-        })}
-      </ul>
-    </>
-  );
-};
-
-export default BookmarkItemList;

+ 1 - 1
packages/app/src/components/UsersHomePageFooter.tsx

@@ -59,7 +59,7 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
       await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
       await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
       await mutateChildBookmarkData();
       await mutateChildBookmarkData();
       setIsCreateAction(false);
       setIsCreateAction(false);
-      toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder') }));
+      toastSuccess(t('toaster.create_succeeded', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);

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

@@ -10,6 +10,7 @@ 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';
 
 
 
 
@@ -163,5 +164,4 @@ Promise<BookmarkFolderDocument> {
   return bookmarkFolder;
   return bookmarkFolder;
 };
 };
 
 
-
 export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);
 export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);

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

@@ -108,6 +108,5 @@ module.exports = (crowi) => {
     }
     }
   });
   });
 
 
-
   return router;
   return router;
 };
 };

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

@@ -1,5 +1,6 @@
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
 $grw-foldertree-item-padding-left: 10px;
 $grw-foldertree-item-padding-left: 10px;
+$grw-bookmark-item-padding-left: 45px;
 
 
 .grw-foldertree {
 .grw-foldertree {
   :global {
   :global {
@@ -67,56 +68,89 @@ $grw-foldertree-item-padding-left: 10px;
       > .list-group-item {
       > .list-group-item {
         padding-left: 0;
         padding-left: 0;
       }
       }
+      > .list-group-item.bookmark-item-list {
+        padding-left: $grw-bookmark-item-padding-left;
+      }
       > .grw-foldertree-item-children {
       > .grw-foldertree-item-children {
         > .grw-foldertree-item-container {
         > .grw-foldertree-item-container {
           > .list-group-item {
           > .list-group-item {
             padding-left: $grw-foldertree-item-padding-left;
             padding-left: $grw-foldertree-item-padding-left;
           }
           }
+          > .list-group-item.bookmark-item-list {
+            padding-left: $grw-foldertree-item-padding-left + $grw-bookmark-item-padding-left;
+          }
           > .grw-foldertree-item-children {
           > .grw-foldertree-item-children {
             > .grw-foldertree-item-container {
             > .grw-foldertree-item-container {
               > .list-group-item {
               > .list-group-item {
                 padding-left: $grw-foldertree-item-padding-left * 2;
                 padding-left: $grw-foldertree-item-padding-left * 2;
               }
               }
+              > .list-group-item.bookmark-item-list {
+                padding-left: ($grw-foldertree-item-padding-left * 2) + $grw-bookmark-item-padding-left;
+              }
               > .grw-foldertree-item-children {
               > .grw-foldertree-item-children {
                 > .grw-foldertree-item-container {
                 > .grw-foldertree-item-container {
                   > .list-group-item {
                   > .list-group-item {
                     padding-left: $grw-foldertree-item-padding-left * 3;
                     padding-left: $grw-foldertree-item-padding-left * 3;
                   }
                   }
+                  > .list-group-item.bookmark-item-list {
+                    padding-left: ($grw-foldertree-item-padding-left * 3) +  $grw-bookmark-item-padding-left;
+                  }
                   > .grw-foldertree-item-children {
                   > .grw-foldertree-item-children {
                     > .grw-foldertree-item-container {
                     > .grw-foldertree-item-container {
                       > .list-group-item {
                       > .list-group-item {
                         padding-left: $grw-foldertree-item-padding-left * 4;
                         padding-left: $grw-foldertree-item-padding-left * 4;
                       }
                       }
+                      > .list-group-item.bookmark-item-list {
+                        padding-left: ($grw-foldertree-item-padding-left * 4) +  $grw-bookmark-item-padding-left;
+                      }
                       > .grw-foldertree-item-children {
                       > .grw-foldertree-item-children {
                         > .grw-foldertree-item-container {
                         > .grw-foldertree-item-container {
                           > .list-group-item {
                           > .list-group-item {
                             padding-left: $grw-foldertree-item-padding-left * 5;
                             padding-left: $grw-foldertree-item-padding-left * 5;
                           }
                           }
+                          > .list-group-item.bookmark-item-list {
+                            padding-left: ($grw-foldertree-item-padding-left * 5) +  $grw-bookmark-item-padding-left;
+                          }
                           > .grw-foldertree-item-children {
                           > .grw-foldertree-item-children {
                             > .grw-foldertree-item-container {
                             > .grw-foldertree-item-container {
                               > .list-group-item {
                               > .list-group-item {
                                 padding-left: $grw-foldertree-item-padding-left * 6;
                                 padding-left: $grw-foldertree-item-padding-left * 6;
                               }
                               }
+                              > .list-group-item.bookmark-item-list {
+                                padding-left:  ($grw-foldertree-item-padding-left * 6) +  $grw-bookmark-item-padding-left;
+                              }
                               > .grw-foldertree-item-children {
                               > .grw-foldertree-item-children {
                                 > .grw-foldertree-item-container {
                                 > .grw-foldertree-item-container {
                                   > .list-group-item {
                                   > .list-group-item {
                                     padding-left: $grw-foldertree-item-padding-left * 7;
                                     padding-left: $grw-foldertree-item-padding-left * 7;
                                   }
                                   }
+                                  > .list-group-item.bookmark-item-list {
+                                    padding-left: ($grw-foldertree-item-padding-left * 7) +  $grw-bookmark-item-padding-left;
+                                  }
                                   > .grw-foldertree-item-children {
                                   > .grw-foldertree-item-children {
                                     > .grw-foldertree-item-container {
                                     > .grw-foldertree-item-container {
                                       > .list-group-item {
                                       > .list-group-item {
                                         padding-left: $grw-foldertree-item-padding-left * 8;
                                         padding-left: $grw-foldertree-item-padding-left * 8;
                                       }
                                       }
+                                      > .list-group-item.bookmark-item-list {
+                                        padding-left: ($grw-foldertree-item-padding-left * 8) +  $grw-bookmark-item-padding-left;
+                                      }
                                       > .grw-foldertree-item-children {
                                       > .grw-foldertree-item-children {
                                         > .grw-foldertree-item-container {
                                         > .grw-foldertree-item-container {
                                           > .list-group-item {
                                           > .list-group-item {
                                             padding-left: $grw-foldertree-item-padding-left * 9;
                                             padding-left: $grw-foldertree-item-padding-left * 9;
                                           }
                                           }
+                                          > .list-group-item.bookmark-item-list {
+                                            padding-left: ($grw-foldertree-item-padding-left * 9) +  $grw-bookmark-item-padding-left;
+                                          }
                                           .grw-foldertree-item-children {
                                           .grw-foldertree-item-children {
                                             > .grw-foldertree-item-container {
                                             > .grw-foldertree-item-container {
                                               > .list-group-item {
                                               > .list-group-item {
                                                 padding-left: $grw-foldertree-item-padding-left * 10;
                                                 padding-left: $grw-foldertree-item-padding-left * 10;
                                               }
                                               }
+                                              > .list-group-item.bookmark-item-list {
+                                                padding-left: ($grw-foldertree-item-padding-left * 10) +  $grw-bookmark-item-padding-left;
+                                              }
                                             }
                                             }
                                           }
                                           }
                                         }
                                         }