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

Merge pull request #7462 from weseek/feat/gw7927-commonize-methods

feat: gw7927 commonize methods
Ryoji Shimizu пре 3 година
родитељ
комит
42d47f523f
21 измењених фајлова са 448 додато и 307 уклоњено
  1. 5 1
      packages/app/public/static/locales/en_US/translation.json
  2. 5 1
      packages/app/public/static/locales/ja_JP/translation.json
  3. 5 1
      packages/app/public/static/locales/zh_CN/translation.json
  4. 34 0
      packages/app/src/client/util/bookmark-utils.ts
  5. 0 15
      packages/app/src/client/util/input-validator-utils.ts
  6. 32 0
      packages/app/src/client/util/input-validator.ts
  7. 82 100
      packages/app/src/components/Bookmarks/BookmarkFolderItem.tsx
  8. 10 6
      packages/app/src/components/Bookmarks/BookmarkFolderMenu.tsx
  9. 15 11
      packages/app/src/components/Bookmarks/BookmarkFolderMenuItem.tsx
  10. 2 2
      packages/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx
  11. 5 0
      packages/app/src/components/Bookmarks/BookmarkFolderTree.module.scss
  12. 12 24
      packages/app/src/components/Bookmarks/BookmarkFolderTree.tsx
  13. 52 60
      packages/app/src/components/Bookmarks/BookmarkItem.tsx
  14. 73 0
      packages/app/src/components/Bookmarks/DragAndDropWrapper.tsx
  15. 9 16
      packages/app/src/components/Common/ClosableTextInput.tsx
  16. 2 2
      packages/app/src/components/DeleteBookmarkFolderModal.tsx
  17. 2 2
      packages/app/src/components/PageList/BookmarkList.tsx
  18. 3 3
      packages/app/src/components/Sidebar/PageTree/Item.tsx
  19. 3 2
      packages/app/src/components/UsersHomePageFooter.tsx
  20. 11 0
      packages/app/src/interfaces/bookmark-info.ts
  21. 86 61
      packages/app/src/styles/molecules/_bookmark-folder-tree.scss

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

@@ -155,8 +155,12 @@
     "error_message": "Some values ​​are incorrect",
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
     "required": "%s is required",
     "invalid_syntax": "The syntax of %s is invalid.",
     "invalid_syntax": "The syntax of %s is invalid.",
-    "title_required": "Title is required."
+    "title_required": "Title is required.",
+    "field_required": "{{target}} is required"
   },
   },
+  "page_name": "Page name",
+  "folder_name": "Folder name",
+  "field": "field",
   "not_creatable_page": {
   "not_creatable_page": {
     "could_not_creata_path": "Couldn't create path."
     "could_not_creata_path": "Couldn't create path."
   },
   },

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

@@ -157,8 +157,12 @@
     "error_message": "いくつかの値が設定されていません",
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
     "required": "%sに値を入力してください",
     "invalid_syntax": "%sの構文が不正です",
     "invalid_syntax": "%sの構文が不正です",
-    "title_required": "タイトルを入力してください"
+    "title_required": "タイトルを入力してください",
+    "field_required": "{{target}}に値を入力してください"
   },
   },
+  "page_name": "ページ名",
+  "folder_name": "フォルダ名",
+  "field": "フィールド",
   "not_creatable_page": {
   "not_creatable_page": {
     "could_not_creata_path": "パスを作成できませんでした。"
     "could_not_creata_path": "パスを作成できませんでした。"
   },
   },

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

@@ -163,8 +163,12 @@
 		"error_message": "有些值不正确",
 		"error_message": "有些值不正确",
 		"required": "%s 是必需的",
 		"required": "%s 是必需的",
 		"invalid_syntax": "%s的语法无效。",
 		"invalid_syntax": "%s的语法无效。",
-    "title_required": "标题是必需的。"
+    "title_required": "标题是必需的。",
+    "field_required": "{{target}} 是必需的"
   },
   },
+  "page_name": "页面名称",
+  "folder_name": "文件夹名称",
+  "field": "字段",
   "not_creatable_page": {
   "not_creatable_page": {
     "could_not_creata_path": "无法创建路径"
     "could_not_creata_path": "无法创建路径"
   },
   },

+ 34 - 0
packages/app/src/client/util/bookmark-utils.ts

@@ -1,6 +1,10 @@
+import { IRevision, Ref } from '@growi/core';
+
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 import { BookmarkFolderItems } from '~/interfaces/bookmark-info';
 
 
+import { apiv3Delete, apiv3Post, apiv3Put } from './apiv3-client';
 
 
+// Check if bookmark folder item has children
 export const hasChildren = (item: BookmarkFolderItems | BookmarkFolderItems[]): boolean => {
 export const hasChildren = (item: BookmarkFolderItems | BookmarkFolderItems[]): boolean => {
   if (item === null) {
   if (item === null) {
     return false;
     return false;
@@ -10,3 +14,33 @@ export const hasChildren = (item: BookmarkFolderItems | BookmarkFolderItems[]):
   }
   }
   return item.children && item.children.length > 0;
   return item.children && item.children.length > 0;
 };
 };
+
+// Add new folder helper
+export const addNewFolder = async(name: string, parent: string | null): Promise<void> => {
+  await apiv3Post('/bookmark-folder', { name, parent });
+};
+
+// Put bookmark to a folder
+export const addBookmarkToFolder = async(pageId: string, folderId: string | null): Promise<void> => {
+  await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId, folderId });
+};
+
+// Delete bookmark folder
+export const deleteBookmarkFolder = async(bookmarkFolderId: string): Promise<void> => {
+  await apiv3Delete(`/bookmark-folder/${bookmarkFolderId}`);
+};
+
+// Rename page from bookmark item control
+export const renamePage = async(pageId: string, revisionId: Ref<IRevision>, newPagePath: string): Promise<void> => {
+  await apiv3Put('/pages/rename', { pageId, revisionId, newPagePath });
+};
+
+// Update bookmark by isBookmarked status
+export const toggleBookmark = async(pageId: string, status: boolean): Promise<void> => {
+  await apiv3Put('/bookmark-folder/update-bookmark', { pageId, status });
+};
+
+// Update Bookmark folder
+export const updateBookmarkFolder = async(bookmarkFolderId: string, name: string, parent: string | null): Promise<void> => {
+  await apiv3Put('/bookmark-folder', { bookmarkFolderId, name, parent });
+};

+ 0 - 15
packages/app/src/client/util/input-validator-utils.ts

@@ -1,15 +0,0 @@
-import i18n from 'i18next';
-
-import { AlertInfo, AlertType } from '~/components/Common/ClosableTextInput';
-
-// Validator for closeable text input
-export const inputValidator = (title: string | null): AlertInfo | null => {
-
-  if (title == null || title === '' || title.trim() === '') {
-    return {
-      type: AlertType.WARNING,
-      message: i18n.t('form_validation.title_required'),
-    };
-  }
-  return null;
-};

+ 32 - 0
packages/app/src/client/util/input-validator.ts

@@ -0,0 +1,32 @@
+export const AlertType = {
+  WARNING: 'warning',
+  ERROR: 'error',
+} as const;
+
+export type AlertType = typeof AlertType[keyof typeof AlertType];
+
+export const ValidationTarget = {
+  FOLDER: 'folder_name',
+  PAGE: 'page_name',
+  DEFAULT: 'field',
+};
+
+export type ValidationTarget = typeof ValidationTarget[keyof typeof ValidationTarget];
+
+export type AlertInfo = {
+  type?: AlertType
+  message?: string,
+  target?: string
+}
+
+export const inputValidator = async(title: string | null, target?: string): Promise<AlertInfo | null> => {
+  const validationTarget = target || ValidationTarget.DEFAULT;
+  if (title == null || title === '' || title.trim() === '') {
+    return {
+      type: AlertType.WARNING,
+      message: 'form_validation.field_required',
+      target: validationTarget,
+    };
+  }
+  return null;
+};

+ 82 - 100
packages/app/src/components/Bookmarks/BookmarkFolderItem.tsx

@@ -3,16 +3,18 @@ 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 { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
-import { hasChildren } from '~/client/util/bookmark-utils';
+import {
+  addBookmarkToFolder, addNewFolder, hasChildren, updateBookmarkFolder,
+} from '~/client/util/bookmark-utils';
 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, DragItemType, DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
-import { IPageHasId, IPageToDeleteWithMeta } from '~/interfaces/page';
+import {
+  BookmarkFolderItems, DragItemDataType, DragItemType, DRAG_ITEM_TYPE,
+} from '~/interfaces/bookmark-info';
+import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { onDeletedBookmarkFolderFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { onDeletedBookmarkFolderFunction, OnDeletedFunction } from '~/interfaces/ui';
 import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
@@ -22,6 +24,7 @@ import { useSWRxCurrentPage } from '~/stores/page';
 import { BookmarkFolderItemControl } from './BookmarkFolderItemControl';
 import { BookmarkFolderItemControl } from './BookmarkFolderItemControl';
 import { BookmarkFolderNameInput } from './BookmarkFolderNameInput';
 import { BookmarkFolderNameInput } from './BookmarkFolderNameInput';
 import { BookmarkItem } from './BookmarkItem';
 import { BookmarkItem } from './BookmarkItem';
+import { DragAndDropWrapper } from './DragAndDropWrapper';
 
 
 
 
 type BookmarkFolderItemProps = {
 type BookmarkFolderItemProps = {
@@ -32,10 +35,6 @@ type BookmarkFolderItemProps = {
   isUserHomePage?: boolean
   isUserHomePage?: boolean
 }
 }
 
 
-type DragItemDataType = {
-  parentFolder: BookmarkFolderItems
-} & BookmarkFolderItemProps & IPageHasId
-
 export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
 export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
   const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const acceptedTypes: DragItemType[] = [DRAG_ITEM_TYPE.FOLDER, DRAG_ITEM_TYPE.BOOKMARK];
   const {
   const {
@@ -60,7 +59,6 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
 
 
   const childrenExists = hasChildren(children);
   const childrenExists = hasChildren(children);
 
 
-
   const loadChildFolder = useCallback(async() => {
   const loadChildFolder = useCallback(async() => {
     setIsOpen(!isOpen);
     setIsOpen(!isOpen);
     setTargetFolder(folderId);
     setTargetFolder(folderId);
@@ -69,7 +67,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   // 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 updateBookmarkFolder(folderId, folderName, parent);
       mutateBookmarkData();
       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' }));
@@ -82,7 +80,7 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
   // Create new folder / subfolder handler
   // Create new folder / subfolder handler
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
     try {
-      await apiv3Post('/bookmark-folder', { name: folderName, parent: targetFolder });
+      await addNewFolder(folderName, targetFolder);
       setIsOpen(true);
       setIsOpen(true);
       setIsCreateAction(false);
       setIsCreateAction(false);
       mutateBookmarkData();
       mutateBookmarkData();
@@ -128,28 +126,15 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     mutateBookmarkInfo();
     mutateBookmarkInfo();
   }, [mutateBookmarkInfo, mutateBookmarkData]);
   }, [mutateBookmarkInfo, mutateBookmarkData]);
 
 
-  const [, bookmarkFolderDragRef] = useDrag({
-    type: DRAG_ITEM_TYPE.FOLDER,
-    item: props,
-    end: (_item, monitor) => {
-      const dropResult = monitor.getDropResult();
-      if (dropResult != null) {
-        mutateBookmarkData();
-      }
-    },
-    collect: monitor => ({
-      isDragging: monitor.isDragging(),
-      canDrag: monitor.canDrag(),
-    }),
-  });
-
 
 
   const itemDropHandler = async(item: DragItemDataType, dragItemType: string | symbol | null) => {
   const itemDropHandler = async(item: DragItemDataType, dragItemType: string | symbol | null) => {
     if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
     if (dragItemType === DRAG_ITEM_TYPE.FOLDER) {
       try {
       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' }));
+        if (item.bookmarkFolder != null) {
+          await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, bookmarkFolder._id);
+          mutateBookmarkData();
+          toastSuccess(t('toaster.update_successed', { target: t('bookmark_folder.bookmark_folder'), ns: 'commons' }));
+        }
       }
       }
       catch (err) {
       catch (err) {
         toastError(err);
         toastError(err);
@@ -157,10 +142,12 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     }
     }
     else {
     else {
       try {
       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' }));
+        if (item != null) {
+          await addBookmarkToFolder(item._id, bookmarkFolder._id);
+          mutateBookmarkData();
+          await mutateUserBookmarks();
+          toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
+        }
       }
       }
       catch (err) {
       catch (err) {
         toastError(err);
         toastError(err);
@@ -168,12 +155,12 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     }
     }
   };
   };
 
 
-  const isDroppable = (item: DragItemDataType, targetRoot: string, targetLevel: number, type: string | null| symbol): boolean => {
+  const isDroppable = (item: DragItemDataType, type: string | null| symbol): boolean => {
     if (type === DRAG_ITEM_TYPE.FOLDER) {
     if (type === DRAG_ITEM_TYPE.FOLDER) {
       if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
       if (item.bookmarkFolder.parent === bookmarkFolder._id || item.bookmarkFolder._id === bookmarkFolder._id) {
         return false;
         return false;
       }
       }
-      return item.root !== targetRoot || item.level >= targetLevel;
+      return item.root !== root || item.level >= level;
     }
     }
 
 
     if (item.parentFolder != null && item.parentFolder._id === bookmarkFolder._id) {
     if (item.parentFolder != null && item.parentFolder._id === bookmarkFolder._id) {
@@ -182,21 +169,6 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
     return true;
     return true;
   };
   };
 
 
-  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 => ({
-      isOver: monitor.isOver({ shallow: true }) && monitor.canDrop(),
-    }),
-  }));
-
 
 
   const renderChildFolder = () => {
   const renderChildFolder = () => {
     return isOpen && children?.map((childFolder) => {
     return isOpen && children?.map((childFolder) => {
@@ -252,61 +224,71 @@ export const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkF
 
 
   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); 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}
+      <DragAndDropWrapper
+        key={folderId}
+        type={acceptedTypes}
+        item={props}
+        useDragMode={true}
+        useDropMode={true}
+        onDropItem={itemDropHandler}
+        isDropable={isDroppable}
       >
       >
-        <div className="grw-triangle-container d-flex justify-content-center">
-          {childrenExists && (
+        <li
+          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">
+            {childrenExists && (
+              <button
+                type="button"
+                className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
+                onClick={loadChildFolder}
+              >
+                <div className="d-flex justify-content-center">
+                  <TriangleIcon />
+                </div>
+              </button>
+            )}
+          </div>
+          {
+            <div>
+              <FolderIcon isOpen={isOpen} />
+            </div>
+          }
+          {isRenameAction ? (
+            <BookmarkFolderNameInput
+              onClickOutside={() => setIsRenameAction(false)}
+              onPressEnter={onPressEnterHandlerForRename}
+              value={name}
+            />
+          ) : (
+            <>
+              <div className='grw-foldertree-title-anchor pl-2' >
+                <p className={'text-truncate m-auto '}>{name}</p>
+              </div>
+            </>
+          )}
+          <div className="grw-foldertree-control d-flex">
+            <BookmarkFolderItemControl
+              onClickRename={onClickRenameHandler}
+              onClickDelete={onClickDeleteHandler}
+            >
+              <div onClick={e => e.stopPropagation()}>
+                <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>
+              </div>
+            </BookmarkFolderItemControl>
             <button
             <button
               type="button"
               type="button"
-              className={`grw-foldertree-triangle-btn btn ${isOpen ? 'grw-foldertree-open' : ''}`}
-              onClick={loadChildFolder}
+              className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
+              onClick={onClickPlusButton}
             >
             >
-              <div className="d-flex justify-content-center">
-                <TriangleIcon />
-              </div>
+              <i className="icon-plus d-block p-0" />
             </button>
             </button>
-          )}
-        </div>
-        {
-          <div>
-            <FolderIcon isOpen={isOpen} />
           </div>
           </div>
-        }
-        {isRenameAction ? (
-          <BookmarkFolderNameInput
-            onClickOutside={() => setIsRenameAction(false)}
-            onPressEnter={onPressEnterHandlerForRename}
-            value={name}
-          />
-        ) : (
-          <>
-            <div className='grw-foldertree-title-anchor pl-2' >
-              <p className={'text-truncate m-auto '}>{name}</p>
-            </div>
-          </>
-        )}
-        <div className="grw-foldertree-control d-flex">
-          <BookmarkFolderItemControl
-            onClickRename={onClickRenameHandler}
-            onClickDelete={onClickDeleteHandler}
-          >
-            <div onClick={e => e.stopPropagation()}>
-              <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>
-            </div>
-          </BookmarkFolderItemControl>
-          <button
-            type="button"
-            className="border-0 rounded btn btn-page-item-control p-0 grw-visible-on-hover"
-            onClick={onClickPlusButton}
-          >
-            <i className="icon-plus d-block p-0" />
-          </button>
-        </div>
-      </li>
+        </li>
+      </DragAndDropWrapper>
       {isCreateAction && (
       {isCreateAction && (
         <div className="flex-fill">
         <div className="flex-fill">
           <BookmarkFolderNameInput
           <BookmarkFolderNameInput

+ 10 - 6
packages/app/src/components/Bookmarks/BookmarkFolderMenu.tsx

@@ -7,7 +7,7 @@ import {
   DropdownItem, DropdownMenu, UncontrolledDropdown,
   DropdownItem, DropdownMenu, UncontrolledDropdown,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
+import { addBookmarkToFolder, addNewFolder, toggleBookmark } from '~/client/util/bookmark-utils';
 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 { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
@@ -33,14 +33,16 @@ export const BookmarkFolderMenu = (props: Props): JSX.Element => {
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: currentPage } = useSWRxCurrentPage();
   const { data: userBookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
   const { data: userBookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(currentPage?._id);
   const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
   const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
-  const isBookmarked = userBookmarkInfo?.isBookmarked;
+  const isBookmarked = userBookmarkInfo?.isBookmarked ?? false;
   const [isOpen, setIsOpen] = useState(false);
   const [isOpen, setIsOpen] = useState(false);
 
 
 
 
   const toggleBookmarkHandler = useCallback(async() => {
   const toggleBookmarkHandler = useCallback(async() => {
 
 
     try {
     try {
-      await apiv3Put('/bookmark-folder/update-bookmark', { pageId: currentPage?._id, status: isBookmarked });
+      if (currentPage != null) {
+        await toggleBookmark(currentPage._id, isBookmarked);
+      }
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -95,7 +97,7 @@ export const BookmarkFolderMenu = (props: Props): JSX.Element => {
 
 
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
     try {
-      await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
+      await addNewFolder(folderName, null);
       await mutateBookmarkFolderData();
       await mutateBookmarkFolderData();
       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' }));
@@ -111,7 +113,9 @@ export const BookmarkFolderMenu = (props: Props): JSX.Element => {
       if (isBookmarked) {
       if (isBookmarked) {
         await toggleBookmarkHandler();
         await toggleBookmarkHandler();
       }
       }
-      await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: itemId });
+      if (currentPage != null) {
+        await addBookmarkToFolder(currentPage._id, itemId);
+      }
       const toaster = isBookmarked ? 'toaster.update_successed' : 'toaster.add_succeeded';
       const toaster = isBookmarked ? 'toaster.update_successed' : 'toaster.add_succeeded';
       toastSuccess(t(toaster, { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
       toastSuccess(t(toaster, { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
       mutateBookmarkInfo();
       mutateBookmarkInfo();
@@ -123,7 +127,7 @@ export const BookmarkFolderMenu = (props: Props): JSX.Element => {
 
 
     mutateBookmarkFolderData();
     mutateBookmarkFolderData();
     setSelectedItem(itemId);
     setSelectedItem(itemId);
-  }, [currentPage?._id, isBookmarked, mutateBookmarkFolderData, mutateBookmarkInfo, mutateUserBookmarks, toggleBookmarkHandler, t]);
+  }, [mutateBookmarkFolderData, isBookmarked, currentPage, t, mutateBookmarkInfo, mutateUserBookmarks, toggleBookmarkHandler]);
 
 
 
 
   const renderBookmarkMenuItem = (child?: BookmarkFolderItems[]) => {
   const renderBookmarkMenuItem = (child?: BookmarkFolderItems[]) => {

+ 15 - 11
packages/app/src/components/Bookmarks/BookmarkFolderMenuItem.tsx

@@ -1,4 +1,6 @@
-import React, { useCallback, useEffect, useState } from 'react';
+import React, {
+  useCallback, useEffect, useState,
+} from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 import {
 import {
@@ -6,8 +8,9 @@ import {
   DropdownMenu, DropdownToggle, UncontrolledDropdown, UncontrolledTooltip,
   DropdownMenu, DropdownToggle, UncontrolledDropdown, UncontrolledTooltip,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
-import { hasChildren } from '~/client/util/bookmark-utils';
+import {
+  addBookmarkToFolder, addNewFolder, hasChildren, toggleBookmark,
+} from '~/client/util/bookmark-utils';
 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';
@@ -21,7 +24,6 @@ import { TriangleIcon } from '../Icons/TriangleIcon';
 
 
 import { BookmarkFolderNameInput } from './BookmarkFolderNameInput';
 import { BookmarkFolderNameInput } from './BookmarkFolderNameInput';
 
 
-
 type Props = {
 type Props = {
   item: BookmarkFolderItems
   item: BookmarkFolderItems
   onSelectedChild: () => void
   onSelectedChild: () => void
@@ -41,13 +43,13 @@ export const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
   const { open: openDeleteBookmarkFolderModal } = useBookmarkFolderDeleteModal();
   const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
   const { mutate: mutateUserBookmarks } = useSWRxCurrentUserBookmarks();
 
 
-  const isBookmarked = userBookmarkInfo?.isBookmarked;
+  const isBookmarked = userBookmarkInfo?.isBookmarked ?? false;
 
 
   const childrenExists = hasChildren(item);
   const childrenExists = hasChildren(item);
 
 
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
     try {
-      await apiv3Post('/bookmark-folder', { name: folderName, parent: item._id });
+      await addNewFolder(folderName, item._id);
       await mutateBookamrkData();
       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' }));
@@ -56,7 +58,7 @@ export const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
       toastError(err);
       toastError(err);
     }
     }
 
 
-  }, [item, mutateBookamrkData, t]);
+  }, [item._id, mutateBookamrkData, t]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (isOpen) {
     if (isOpen) {
@@ -106,10 +108,12 @@ export const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
     e.stopPropagation();
     e.stopPropagation();
     onSelectedChild();
     onSelectedChild();
     try {
     try {
-      if (isBookmarked) {
-        await apiv3Put('/bookmark-folder/update-bookmark', { pageId: currentPage?._id, status: isBookmarked });
+      if (isBookmarked && currentPage != null) {
+        await toggleBookmark(currentPage._id, isBookmarked);
+      }
+      if (currentPage != null) {
+        await addBookmarkToFolder(currentPage._id, item._id);
       }
       }
-      await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: currentPage?._id, folderId: item._id });
       const toaster = isBookmarked ? 'toaster.update_successed' : 'toaster.add_succeeded';
       const toaster = isBookmarked ? 'toaster.update_successed' : 'toaster.add_succeeded';
       toastSuccess(t(toaster, { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
       toastSuccess(t(toaster, { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
       mutateUserBookmarks();
       mutateUserBookmarks();
@@ -120,7 +124,7 @@ export const BookmarkFolderMenuItem = (props: Props): JSX.Element => {
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
-  }, [onSelectedChild, isBookmarked, mutateBookamrkData, mutateBookmarkInfo, currentPage?._id, mutateUserBookmarks, t]);
+  }, [onSelectedChild, isBookmarked, currentPage, t, mutateUserBookmarks, mutateBookamrkData, mutateBookmarkInfo]);
 
 
   const renderBookmarkSubMenuItem = useCallback(() => {
   const renderBookmarkSubMenuItem = useCallback(() => {
     if (!isOpen) {
     if (!isOpen) {

+ 2 - 2
packages/app/src/components/Bookmarks/BookmarkFolderNameInput.tsx

@@ -1,6 +1,6 @@
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-import { inputValidator } from '~/client/util/input-validator-utils';
+import { inputValidator, ValidationTarget } from '~/client/util/input-validator';
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
 import ClosableTextInput from '~/components/Common/ClosableTextInput';
 
 
 
 
@@ -23,7 +23,7 @@ export const BookmarkFolderNameInput = (props: Props): JSX.Element => {
         placeholder={t('bookmark_folder.input_placeholder')}
         placeholder={t('bookmark_folder.input_placeholder')}
         onClickOutside={onClickOutside}
         onClickOutside={onClickOutside}
         onPressEnter={onPressEnter}
         onPressEnter={onPressEnter}
-        inputValidator={inputValidator}
+        validationTarget={ValidationTarget.FOLDER}
       />
       />
     </div>
     </div>
   );
   );

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

@@ -14,4 +14,9 @@
       padding: 0.7rem;
       padding: 0.7rem;
     }
     }
   }
   }
+  .grw-drag-drop-container > .grw-drop-item-area {
+    margin: 1rem;
+    border-style: dashed !important;
+    border-width: 0.15rem !important;
+  }
 }
 }

+ 12 - 24
packages/app/src/components/Bookmarks/BookmarkFolderTree.tsx

@@ -2,9 +2,8 @@
 import { useCallback } from 'react';
 import { useCallback } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
-import { useDrop } from 'react-dnd';
 
 
-import { apiv3Post, apiv3Put } from '~/client/util/apiv3-client';
+import { addBookmarkToFolder, updateBookmarkFolder } from '~/client/util/bookmark-utils';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { BookmarkFolderItems, DragItemType, DRAG_ITEM_TYPE } 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';
@@ -16,6 +15,7 @@ import { useSWRxCurrentPage } from '~/stores/page';
 
 
 import { BookmarkFolderItem } from './BookmarkFolderItem';
 import { BookmarkFolderItem } from './BookmarkFolderItem';
 import { BookmarkItem } from './BookmarkItem';
 import { BookmarkItem } from './BookmarkItem';
+import { DragAndDropWrapper } from './DragAndDropWrapper';
 
 
 import styles from './BookmarkFolderTree.module.scss';
 import styles from './BookmarkFolderTree.module.scss';
 
 
@@ -69,7 +69,7 @@ export const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element
   const itemDropHandler = async(item: DragItemDataType, dragType: string | null | symbol) => {
   const itemDropHandler = async(item: DragItemDataType, dragType: string | null | symbol) => {
     if (dragType === DRAG_ITEM_TYPE.FOLDER) {
     if (dragType === DRAG_ITEM_TYPE.FOLDER) {
       try {
       try {
-        await apiv3Put('/bookmark-folder', { bookmarkFolderId: item.bookmarkFolder._id, name: item.bookmarkFolder.name, parent: null });
+        await updateBookmarkFolder(item.bookmarkFolder._id, item.bookmarkFolder.name, null);
         await mutateBookamrkData();
         await mutateBookamrkData();
         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' }));
       }
       }
@@ -79,7 +79,7 @@ export const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element
     }
     }
     else {
     else {
       try {
       try {
-        await apiv3Post('/bookmark-folder/add-boookmark-to-folder', { pageId: item._id, folderId: null });
+        await addBookmarkToFolder(item._id, null);
         await mutateUserBookmarks();
         await mutateUserBookmarks();
         toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
         toastSuccess(t('toaster.add_succeeded', { target: t('bookmark_folder.bookmark'), ns: 'commons' }));
       }
       }
@@ -99,23 +99,6 @@ export const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element
 
 
   };
   };
 
 
-  const [, 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 (
     <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}` } >
     <div className={`grw-folder-tree-container ${styles['grw-folder-tree-container']}` } >
       <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group px-2 py-2`}>
       <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group px-2 py-2`}>
@@ -145,13 +128,18 @@ export const BookmarkFolderTree = (props: BookmarkFolderTreeProps): JSX.Element
         ))}
         ))}
       </ul>
       </ul>
       {bookmarkFolderData != null && bookmarkFolderData.length > 0 && (
       {bookmarkFolderData != null && bookmarkFolderData.length > 0 && (
-        <div ref={(c) => { dropRef(c) }} className="grw-drop-item-area">
-          <div className="grw-accept-drop-item">
+        <DragAndDropWrapper
+          useDropMode={true}
+          type={acceptedTypes}
+          onDropItem={itemDropHandler}
+          isDropable={isDroppable}
+        >
+          <div className="grw-drop-item-area">
             <div className="d-flex flex-column align-items-center">
             <div className="d-flex flex-column align-items-center">
               {t('bookmark_folder.drop_item_here')}
               {t('bookmark_folder.drop_item_here')}
             </div>
             </div>
           </div>
           </div>
-        </div>
+        </DragAndDropWrapper>
       )}
       )}
     </div>
     </div>
   );
   );

+ 52 - 60
packages/app/src/components/Bookmarks/BookmarkItem.tsx

@@ -3,15 +3,14 @@ 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 { inputValidator } from '~/client/util/input-validator-utils';
+import { renamePage } from '~/client/util/bookmark-utils';
+import { ValidationTarget } from '~/client/util/input-validator';
 import { toastError, toastSuccess } from '~/client/util/toastr';
 import { toastError, toastSuccess } from '~/client/util/toastr';
-import { BookmarkFolderItems, DRAG_ITEM_TYPE } from '~/interfaces/bookmark-info';
+import { BookmarkFolderItems, DragItemDataType, 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';
@@ -20,6 +19,7 @@ import ClosableTextInput from '../Common/ClosableTextInput';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
 import { PageListItemS } from '../PageList/PageListItemS';
 import { PageListItemS } from '../PageList/PageListItemS';
 
 
+import { DragAndDropWrapper } from './DragAndDropWrapper';
 
 
 type Props = {
 type Props = {
   bookmarkedPage: IPageHasId,
   bookmarkedPage: IPageHasId,
@@ -41,6 +41,10 @@ export const BookmarkItem = (props: Props): JSX.Element => {
   const { mutate: mutateBookamrkData } = useSWRxBookamrkFolderAndChild();
   const { mutate: mutateBookamrkData } = useSWRxBookamrkFolderAndChild();
   const { data: fetchedPageInfo } = useSWRxPageInfo(bookmarkedPage._id);
   const { data: fetchedPageInfo } = useSWRxPageInfo(bookmarkedPage._id);
 
 
+  const dragItem: Partial<DragItemDataType> = {
+    ...bookmarkedPage, parentFolder,
+  };
+
   useEffect(() => {
   useEffect(() => {
     mutateBookamrkData();
     mutateBookamrkData();
   }, [mutateBookamrkData]);
   }, [mutateBookamrkData]);
@@ -65,11 +69,7 @@ export const BookmarkItem = (props: Props): JSX.Element => {
 
 
     try {
     try {
       setRenameInputShown(false);
       setRenameInputShown(false);
-      await apiv3Put('/pages/rename', {
-        pageId: bookmarkedPage._id,
-        revisionId: bookmarkedPage.revision,
-        newPagePath,
-      });
+      await renamePage(bookmarkedPage._id, bookmarkedPage.revision, newPagePath);
       onRenamed();
       onRenamed();
       toastSuccess(t('renamed_pages', { path: bookmarkedPage.path }));
       toastSuccess(t('renamed_pages', { path: bookmarkedPage.path }));
     }
     }
@@ -96,59 +96,51 @@ export const BookmarkItem = (props: Props): JSX.Element => {
     onClickDeleteMenuItem(pageToDelete);
     onClickDeleteMenuItem(pageToDelete);
   }, [bookmarkedPage, onClickDeleteMenuItem]);
   }, [bookmarkedPage, onClickDeleteMenuItem]);
 
 
-  const [, bookmarkItemDragRef] = useDrag({
-    type: DRAG_ITEM_TYPE.BOOKMARK,
-    item: { ...bookmarkedPage, parentFolder },
-    end: (_, monitor) => {
-      if (monitor.getDropResult() != null) {
-        mutateBookamrkData();
-      }
-    },
-    collect: monitor => ({
-      isDragging: monitor.isDragging(),
-      canDrag: monitor.canDrag(),
-    }),
-  });
-
   return (
   return (
-    <li
-      className="bookmark-item-list list-group-item list-group-item-action border-0 py-0 mr-auto d-flex align-items-center"
-      key={bookmarkedPage._id} ref={(c) => { bookmarkItemDragRef(c) }}
-      id={bookmarkItemId}
+    <DragAndDropWrapper
+      item={dragItem}
+      type={[DRAG_ITEM_TYPE.BOOKMARK]}
+      useDragMode={true}
     >
     >
-      { isRenameInputShown ? (
-        <ClosableTextInput
-          value={nodePath.basename(bookmarkedPage.path ?? '')}
-          placeholder={t('Input page name')}
-          onClickOutside={() => { setRenameInputShown(false) }}
-          onPressEnter={pressEnterForRenameHandler}
-          inputValidator={inputValidator}
-        />
-      ) : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle}/>}
-      <div className='grw-foldertree-control'>
-        <PageItemControl
-          pageId={bookmarkedPage._id}
-          isEnableActions
-          pageInfo={fetchedPageInfo}
-          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>
-      </div>
-      <UncontrolledTooltip
-        modifiers={{ preventOverflow: { boundariesElement: 'window' } }}
-        autohide={false}
-        placement="right"
-        target={bookmarkItemId}
-        fade={false}
+      <li
+        className="bookmark-item-list list-group-item list-group-item-action border-0 py-0 mr-auto d-flex align-items-center"
+        key={bookmarkedPage._id}
+        id={bookmarkItemId}
       >
       >
-        {formerPagePath !== null ? `${formerPagePath}/` : '/'}
-      </UncontrolledTooltip>
-    </li>
+        { isRenameInputShown ? (
+          <ClosableTextInput
+            value={nodePath.basename(bookmarkedPage.path ?? '')}
+            placeholder={t('Input page name')}
+            onClickOutside={() => { setRenameInputShown(false) }}
+            onPressEnter={pressEnterForRenameHandler}
+            validationTarget={ValidationTarget.PAGE}
+          />
+        ) : <PageListItemS page={bookmarkedPage} pageTitle={pageTitle}/>}
+        <div className='grw-foldertree-control'>
+          <PageItemControl
+            pageId={bookmarkedPage._id}
+            isEnableActions
+            pageInfo={fetchedPageInfo}
+            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>
+        </div>
+        <UncontrolledTooltip
+          modifiers={{ preventOverflow: { boundariesElement: 'window' } }}
+          autohide={false}
+          placement="right"
+          target={bookmarkItemId}
+          fade={false}
+        >
+          {formerPagePath !== null ? `${formerPagePath}/` : '/'}
+        </UncontrolledTooltip>
+      </li>
+    </DragAndDropWrapper>
   );
   );
 };
 };

+ 73 - 0
packages/app/src/components/Bookmarks/DragAndDropWrapper.tsx

@@ -0,0 +1,73 @@
+import React, { ReactNode } from 'react';
+
+import { useDrag, useDrop } from 'react-dnd';
+
+import { DragItemDataType } from '~/interfaces/bookmark-info';
+
+type DragAndDropWrapperProps = {
+  item?: Partial<DragItemDataType>
+  type: string[]
+  children: ReactNode
+  useDragMode?: boolean
+  useDropMode?: boolean
+  onDropItem?:(item: DragItemDataType, type: string | null | symbol) => Promise<void>
+  isDropable?:(item: Partial<DragItemDataType>, type: string | null | symbol) => boolean
+}
+
+export const DragAndDropWrapper = (props: DragAndDropWrapperProps): JSX.Element => {
+  const {
+    item, children, useDragMode, useDropMode, type, onDropItem, isDropable,
+  } = props;
+
+
+  const acceptedTypes = type;
+  const sourcetype: string | symbol = type[0];
+
+
+  const [, dragRef] = useDrag({
+    type: sourcetype,
+    item,
+    collect: monitor => ({
+      isDragging: monitor.isDragging(),
+      canDrag: monitor.canDrag(),
+    }),
+  });
+
+  const [{ isOver }, dropRef] = useDrop(() => ({
+    accept: acceptedTypes,
+    drop: (item: DragItemDataType, monitor) => {
+      const itemType: string | null | symbol = monitor.getItemType();
+      if (onDropItem != null) {
+        onDropItem(item, itemType);
+      }
+    },
+    canDrop: (item, monitor) => {
+      const itemType: string | null | symbol = monitor.getItemType();
+      if (isDropable != null) {
+        return isDropable(item, itemType);
+      }
+      return false;
+    },
+    collect: monitor => ({
+      isOver: monitor.isOver({ shallow: true }) && monitor.canDrop(),
+    }),
+  }));
+
+
+  const getRef = (c: HTMLDivElement | null) => {
+    if (useDragMode && useDropMode) {
+      return [dragRef(c), dropRef(c)];
+    } if (useDragMode) {
+      return dragRef(c);
+    } if (useDropMode) {
+      return dropRef(c);
+    }
+    return null;
+  };
+
+  return (
+    <div ref={c => getRef(c)} className={`grw-drag-drop-container ${isOver ? 'grw-accept-drop-item' : ''}` }>
+      {children}
+    </div>
+  );
+};

+ 9 - 16
packages/app/src/components/Common/ClosableTextInput.tsx

@@ -4,40 +4,33 @@ import React, {
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
-export const AlertType = {
-  WARNING: 'warning',
-  ERROR: 'error',
-} as const;
-
-export type AlertType = typeof AlertType[keyof typeof AlertType];
-
-export type AlertInfo = {
-  type?: AlertType
-  message?: string
-}
+import { AlertInfo, AlertType, inputValidator } from '~/client/util/input-validator';
 
 
 type ClosableTextInputProps = {
 type ClosableTextInputProps = {
   value?: string
   value?: string
   placeholder?: string
   placeholder?: string
-  inputValidator?(text: string): AlertInfo | Promise<AlertInfo> | null
+  validationTarget?: string,
   onPressEnter?(inputText: string | null): void
   onPressEnter?(inputText: string | null): void
   onClickOutside?(): void
   onClickOutside?(): void
 }
 }
 
 
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
 const ClosableTextInput: FC<ClosableTextInputProps> = memo((props: ClosableTextInputProps) => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const inputRef = useRef<HTMLInputElement>(null);
+  const { validationTarget } = props;
 
 
+  const inputRef = useRef<HTMLInputElement>(null);
   const [inputText, setInputText] = useState(props.value);
   const [inputText, setInputText] = useState(props.value);
   const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
   const [currentAlertInfo, setAlertInfo] = useState<AlertInfo | null>(null);
   const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
   const [isAbleToShowAlert, setIsAbleToShowAlert] = useState(false);
   const [isComposing, setComposing] = useState(false);
   const [isComposing, setComposing] = useState(false);
 
 
+
   const createValidation = async(inputText: string) => {
   const createValidation = async(inputText: string) => {
-    if (props.inputValidator != null) {
-      const alertInfo = await props.inputValidator(inputText);
-      setAlertInfo(alertInfo);
+    const alertInfo = await inputValidator(inputText, validationTarget);
+    if (alertInfo && alertInfo.message != null && alertInfo.target != null) {
+      alertInfo.message = t(alertInfo.message, { target: t(alertInfo.target) });
     }
     }
+    setAlertInfo(alertInfo);
   };
   };
 
 
   const onChangeHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {
   const onChangeHandler = async(e: React.ChangeEvent<HTMLInputElement>) => {

+ 2 - 2
packages/app/src/components/DeleteBookmarkFolderModal.tsx

@@ -6,7 +6,7 @@ import {
   Modal, ModalBody, ModalFooter, ModalHeader,
   Modal, ModalBody, ModalFooter, ModalHeader,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-import { apiv3Delete } from '~/client/util/apiv3-client';
+import { deleteBookmarkFolder } from '~/client/util/bookmark-utils';
 import { toastError } from '~/client/util/toastr';
 import { toastError } from '~/client/util/toastr';
 import { FolderIcon } from '~/components/Icons/FolderIcon';
 import { FolderIcon } from '~/components/Icons/FolderIcon';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
 import { useBookmarkFolderDeleteModal } from '~/stores/modal';
@@ -23,7 +23,7 @@ const DeleteBookmarkFolderModal: FC = () => {
     }
     }
     if (deleteBookmarkFolderModalData.bookmarkFolder != null) {
     if (deleteBookmarkFolderModalData.bookmarkFolder != null) {
       try {
       try {
-        await apiv3Delete(`/bookmark-folder/${deleteBookmarkFolderModalData.bookmarkFolder._id}`);
+        await deleteBookmarkFolder(deleteBookmarkFolderModalData.bookmarkFolder._id);
         const onDeleted = deleteBookmarkFolderModalData.opts?.onDeleted;
         const onDeleted = deleteBookmarkFolderModalData.opts?.onDeleted;
         if (onDeleted != null) {
         if (onDeleted != null) {
           onDeleted(deleteBookmarkFolderModalData.bookmarkFolder._id);
           onDeleted(deleteBookmarkFolderModalData.bookmarkFolder._id);

+ 2 - 2
packages/app/src/components/PageList/BookmarkList.tsx

@@ -11,7 +11,7 @@ import { DropdownToggle } from 'reactstrap';
 import { unbookmark } from '~/client/services/page-operation';
 import { unbookmark } from '~/client/services/page-operation';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put } from '~/client/util/apiv3-client';
 import { apiv3Put } from '~/client/util/apiv3-client';
-import { inputValidator } from '~/client/util/input-validator-utils';
+import { ValidationTarget } from '~/client/util/input-validator';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
@@ -89,7 +89,7 @@ export const BookmarkList = (props:Props): JSX.Element => {
           placeholder={t('Input page name')}
           placeholder={t('Input page name')}
           onClickOutside={() => { setIsRenameInputShown(false) }}
           onClickOutside={() => { setIsRenameInputShown(false) }}
           onPressEnter={pressEnterForRenameHandler}
           onPressEnter={pressEnterForRenameHandler}
-          inputValidator={inputValidator}
+          validationTarget={ValidationTarget.PAGE}
         />
         />
       ) : (
       ) : (
         <PageListItemS page={page} />
         <PageListItemS page={page} />

+ 3 - 3
packages/app/src/components/Sidebar/PageTree/Item.tsx

@@ -13,7 +13,7 @@ import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { bookmark, unbookmark, resumeRenameOperation } from '~/client/services/page-operation';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { toastWarning, toastError, toastSuccess } from '~/client/util/apiNotification';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
 import { apiv3Put, apiv3Post } from '~/client/util/apiv3-client';
-import { inputValidator } from '~/client/util/input-validator-utils';
+import { ValidationTarget } from '~/client/util/input-validator';
 import { TriangleIcon } from '~/components/Icons/TriangleIcon';
 import { TriangleIcon } from '~/components/Icons/TriangleIcon';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import { NotAvailableForGuest } from '~/components/NotAvailableForGuest';
 import {
 import {
@@ -451,7 +451,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
                   placeholder={t('Input page name')}
                   placeholder={t('Input page name')}
                   onClickOutside={() => { setRenameInputShown(false) }}
                   onClickOutside={() => { setRenameInputShown(false) }}
                   onPressEnter={onPressEnterForRenameHandler}
                   onPressEnter={onPressEnterForRenameHandler}
-                  inputValidator={inputValidator}
+                  validationTarget={ValidationTarget.PAGE}
                 />
                 />
               </NotDraggableForClosableTextInput>
               </NotDraggableForClosableTextInput>
             </div>
             </div>
@@ -523,7 +523,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
               placeholder={t('Input page name')}
               placeholder={t('Input page name')}
               onClickOutside={() => { setNewPageInputShown(false) }}
               onClickOutside={() => { setNewPageInputShown(false) }}
               onPressEnter={onPressEnterForCreateHandler}
               onPressEnter={onPressEnterForCreateHandler}
-              inputValidator={inputValidator}
+              validationTarget={ValidationTarget.PAGE}
             />
             />
           </NotDraggableForClosableTextInput>
           </NotDraggableForClosableTextInput>
         </div>
         </div>

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

@@ -1,4 +1,4 @@
-import React, { useCallback, useState } from 'react';
+import React, { useCallback, useMemo, useState } from 'react';
 
 
 import { useTranslation } from 'next-i18next';
 import { useTranslation } from 'next-i18next';
 
 
@@ -15,6 +15,7 @@ import { BookmarkFolderTree } from './Bookmarks/BookmarkFolderTree';
 import { CompressIcon } from './Icons/CompressIcon';
 import { CompressIcon } from './Icons/CompressIcon';
 import { ExpandIcon } from './Icons/ExpandIcon';
 import { ExpandIcon } from './Icons/ExpandIcon';
 import { FolderPlusIcon } from './Icons/FolderPlusIcon';
 import { FolderPlusIcon } from './Icons/FolderPlusIcon';
+import { addNewFolder } from '~/client/util/bookmark-utils';
 
 
 
 
 export type UsersHomePageFooterProps = {
 export type UsersHomePageFooterProps = {
@@ -30,7 +31,7 @@ export const UsersHomePageFooter = (props: UsersHomePageFooterProps): JSX.Elemen
 
 
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
   const onPressEnterHandlerForCreate = useCallback(async(folderName: string) => {
     try {
     try {
-      await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
+      await addNewFolder(folderName, null);
       await mutateChildBookmarkData();
       await mutateChildBookmarkData();
       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' }));

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

@@ -37,4 +37,15 @@ export const DRAG_ITEM_TYPE = {
   BOOKMARK: 'BOOKMARK',
   BOOKMARK: 'BOOKMARK',
 } as const;
 } as const;
 
 
+type BookmarkDragItem = {
+  bookmarkFolder: BookmarkFolderItems
+  level: number
+  root: string
+}
+
+export type DragItemDataType = BookmarkDragItem & {
+  parentFolder: BookmarkFolderItems | null
+} & IPageHasId
+
+
 export type DragItemType = typeof DRAG_ITEM_TYPE[keyof typeof DRAG_ITEM_TYPE];
 export type DragItemType = typeof DRAG_ITEM_TYPE[keyof typeof DRAG_ITEM_TYPE];

+ 86 - 61
packages/app/src/styles/molecules/_bookmark-folder-tree.scss

@@ -1,6 +1,6 @@
 @use '~/styles/variables' as var;
 @use '~/styles/variables' as var;
-$grw-foldertree-item-padding-left: 10px;
-$grw-bookmark-item-padding-left: 45px;
+$grw-foldertree-item-padding-left: 15px;
+$grw-bookmark-item-padding-left: 35px;
 
 
 .grw-foldertree {
 .grw-foldertree {
   :global {
   :global {
@@ -69,96 +69,121 @@ $grw-bookmark-item-padding-left: 45px;
   }
   }
   &:global{
   &:global{
     // To realize a hierarchical structure, set multiplied padding-left to each foldertree-item
     // To realize a hierarchical structure, set multiplied padding-left to each foldertree-item
-    > .grw-foldertree-item-container {
-      > .list-group-item {
-        padding-left: 0;
-      } &.grw-root-bookmarks{
-        .list-group-item.bookmark-item-list {
-          padding-left: $grw-foldertree-item-padding-left + 25;
+    >.grw-foldertree-item-container.grw-root-bookmarks{
+      > .grw-drag-drop-container {
+        > .list-group-item.bookmark-item-list {
+          padding-left: $grw-bookmark-item-padding-left ;
         }
         }
       }
       }
-      > .list-group-item.bookmark-item-list {
-        padding-left: $grw-bookmark-item-padding-left;
+    }
+    > .grw-foldertree-item-container {
+      > .grw-drag-drop-container {
+        > .list-group-item {
+          padding-left: 0
+        }
+        > .list-group-item.bookmark-item-list {
+          padding-left: $grw-bookmark-item-padding-left + $grw-foldertree-item-padding-left;
+        }
       }
       }
       > .grw-foldertree-item-children {
       > .grw-foldertree-item-children {
         > .grw-foldertree-item-container {
         > .grw-foldertree-item-container {
-          > .list-group-item {
-            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-drag-drop-container {
+            > .list-group-item {
+              padding: $grw-foldertree-item-padding-left
+            }
+            > .list-group-item.bookmark-item-list {
+              padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 2);
+            }
           }
           }
           > .grw-foldertree-item-children {
           > .grw-foldertree-item-children {
             > .grw-foldertree-item-container {
             > .grw-foldertree-item-container {
-              > .list-group-item {
-                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-drag-drop-container {
+                > .list-group-item {
+                  padding: $grw-foldertree-item-padding-left * 2
+                }
+                > .list-group-item.bookmark-item-list {
+                  padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 3);
+                }
               }
               }
               > .grw-foldertree-item-children {
               > .grw-foldertree-item-children {
                 > .grw-foldertree-item-container {
                 > .grw-foldertree-item-container {
-                  > .list-group-item {
-                    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-drag-drop-container {
+                    > .list-group-item {
+                      padding: $grw-foldertree-item-padding-left * 3
+                    }
+                    > .list-group-item.bookmark-item-list {
+                      padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 4);
+                    }
                   }
                   }
                   > .grw-foldertree-item-children {
                   > .grw-foldertree-item-children {
                     > .grw-foldertree-item-container {
                     > .grw-foldertree-item-container {
-                      > .list-group-item {
-                        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-drag-drop-container {
+                        > .list-group-item {
+                          padding: $grw-foldertree-item-padding-left * 4
+                        }
+                        > .list-group-item.bookmark-item-list {
+                          padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 5);
+                        }
                       }
                       }
                       > .grw-foldertree-item-children {
                       > .grw-foldertree-item-children {
                         > .grw-foldertree-item-container {
                         > .grw-foldertree-item-container {
-                          > .list-group-item {
-                            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-drag-drop-container {
+                            > .list-group-item {
+                              padding: $grw-foldertree-item-padding-left * 5
+                            }
+                            > .list-group-item.bookmark-item-list {
+                              padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 6);
+                            }
                           }
                           }
                           > .grw-foldertree-item-children {
                           > .grw-foldertree-item-children {
                             > .grw-foldertree-item-container {
                             > .grw-foldertree-item-container {
-                              > .list-group-item {
-                                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-drag-drop-container {
+                                > .list-group-item {
+                                  padding: $grw-foldertree-item-padding-left * 6
+                                }
+                                > .list-group-item.bookmark-item-list {
+                                  padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 7);
+                                }
                               }
                               }
                               > .grw-foldertree-item-children {
                               > .grw-foldertree-item-children {
                                 > .grw-foldertree-item-container {
                                 > .grw-foldertree-item-container {
-                                  > .list-group-item {
-                                    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-drag-drop-container {
+                                    > .list-group-item {
+                                      padding: $grw-foldertree-item-padding-left * 7
+                                    }
+                                    > .list-group-item.bookmark-item-list {
+                                      padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 8);
+                                    }
                                   }
                                   }
                                   > .grw-foldertree-item-children {
                                   > .grw-foldertree-item-children {
                                     > .grw-foldertree-item-container {
                                     > .grw-foldertree-item-container {
-                                      > .list-group-item {
-                                        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-drag-drop-container {
+                                        > .list-group-item {
+                                          padding: $grw-foldertree-item-padding-left * 8
+                                        }
+                                        > .list-group-item.bookmark-item-list {
+                                          padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 9);
+                                        }
                                       }
                                       }
                                       > .grw-foldertree-item-children {
                                       > .grw-foldertree-item-children {
                                         > .grw-foldertree-item-container {
                                         > .grw-foldertree-item-container {
-                                          > .list-group-item {
-                                            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-drag-drop-container {
+                                            > .list-group-item {
+                                              padding: $grw-foldertree-item-padding-left * 9
+                                            }
+                                            > .list-group-item.bookmark-item-list {
+                                              padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 10);
+                                            }
                                           }
                                           }
-                                          .grw-foldertree-item-children {
+                                          > .grw-foldertree-item-children {
                                             > .grw-foldertree-item-container {
                                             > .grw-foldertree-item-container {
-                                              > .list-group-item {
-                                                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;
+                                              > .grw-drag-drop-container {
+                                                > .list-group-item {
+                                                  padding: $grw-foldertree-item-padding-left * 10
+                                                }
+                                                > .list-group-item.bookmark-item-list {
+                                                  padding-left: $grw-bookmark-item-padding-left + ($grw-foldertree-item-padding-left * 11);
+                                                }
                                               }
                                               }
                                             }
                                             }
                                           }
                                           }