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

Merge pull request #6897 from weseek/feat/gw7904-add-plus-button-on-bookmark-item

feat: gw7904 add plus button on bookmark folder item
Kaori Tokashiki 3 лет назад
Родитель
Сommit
b8bcc21968

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

@@ -1,14 +1,12 @@
 
-import React, { useState } from 'react';
+import React from 'react';
 
 import { useTranslation } from 'react-i18next';
 
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
-import { apiv3Post } from '~/client/util/apiv3-client';
+import { toastSuccess } from '~/client/util/apiNotification';
 import { IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
 import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
-import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 import { useIsGuestUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
 
@@ -20,11 +18,8 @@ const Bookmarks = () : JSX.Element => {
   const { t } = useTranslation();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: currentUserBookmarksData, mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
-  const { mutate: mutateInitialBookmarkFolderData } = useSWRxBookamrkFolderAndChild(true);
   const { open: openDeleteModal } = usePageDeleteModal();
 
-  const [isRenameFolderShown, setIsRenameFolderShown] = useState<boolean>(false);
-  const [folderName, setFolderName] = useState<string>('');
 
   const deleteMenuItemClickHandler = (pageToDelete: IPageToDeleteWithMeta) => {
     const pageDeletedHandler : OnDeletedFunction = (pathOrPathsToDelete, _isRecursively, isCompletely) => {
@@ -44,24 +39,6 @@ const Bookmarks = () : JSX.Element => {
     openDeleteModal([pageToDelete], { onDeleted: pageDeletedHandler });
   };
 
-  const onPressEnterHandler = async(folderName: string) => {
-    setFolderName(folderName);
-    try {
-      await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
-      setIsRenameFolderShown(false);
-      setFolderName('');
-      mutateInitialBookmarkFolderData();
-      toastSuccess(t('Create New Bookmark Folder Success'));
-    }
-    catch (err) {
-      toastError(err);
-    }
-  };
-
-  const onClickOutsideHandler = () => {
-    setIsRenameFolderShown(false);
-    setFolderName('');
-  };
 
   const renderBookmarkList = () => {
     if (currentUserBookmarksData?.length === 0) {
@@ -97,13 +74,7 @@ const Bookmarks = () : JSX.Element => {
       </div>
       {!isGuestUser && (
         <>
-          <BookmarkFolder
-            onClickNewFolder={() => setIsRenameFolderShown(true)}
-            isRenameInputShown={isRenameFolderShown}
-            onClickOutside={onClickOutsideHandler}
-            onPressEnter={onPressEnterHandler}
-            folderName={folderName}
-          />
+          <BookmarkFolder />
         </>
       )
       }

+ 34 - 21
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolder.tsx

@@ -1,32 +1,47 @@
-
-import React from 'react';
+import React, { useCallback, useState } from 'react';
 
 import { useTranslation } from 'next-i18next';
 
-import ClosableTextInput from '~/components/Common/ClosableTextInput';
+import { toastError, toastSuccess } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
 import FolderPlusIcon from '~/components/Icons/FolderPlusIcon';
+import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 
+import BookmarkFolderNameInput from './BookmarkFolderNameInput';
 import BookmarkFolderTree from './BookmarkFolderTree';
 
-type Props = {
-  onClickNewFolder: () => void
-  isRenameInputShown: boolean
-  folderName: string
-  onClickOutside: () => void
-  onPressEnter: (folderName: string) => void
-}
-const BookmarkFolder = (props: Props): JSX.Element => {
-  const {
-    onClickNewFolder, isRenameInputShown, folderName, onClickOutside, onPressEnter,
-  } = props;
+
+const BookmarkFolder = (): JSX.Element => {
+
   const { t } = useTranslation();
+  const [isRenameInputShown, setIsRenameInputShown] = useState<boolean>(false);
+  const { mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(null);
+
+
+  const onClickBookmarkFolder = () => {
+    setIsRenameInputShown(true);
+  };
+
+  const onPressEnterHandler = useCallback(async(folderName: string) => {
+
+    try {
+      await apiv3Post('/bookmark-folder', { name: folderName, parent: null });
+      await mutateChildBookmarkData();
+      setIsRenameInputShown(false);
+      toastSuccess(t('Create New Bookmark Folder Success'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+  }, [mutateChildBookmarkData, t]);
 
   return (
     <>
       <div className="col-8 mb-2 ">
         <button
           className="btn btn-block btn-outline-secondary rounded-pill d-flex justify-content-start align-middle"
-          onClick={onClickNewFolder}
+          onClick={onClickBookmarkFolder}
         >
           <FolderPlusIcon />
           <span className="mx-2 ">New Folder</span>
@@ -34,12 +49,10 @@ const BookmarkFolder = (props: Props): JSX.Element => {
       </div>
       {
         isRenameInputShown && (
-          <div className="col-10 mb-2 ml-2 ">
-            <ClosableTextInput
-              value={folderName}
-              placeholder={t('Input Folder name')}
-              onClickOutside={onClickOutside}
-              onPressEnter={onPressEnter}
+          <div className="col-12 mb-2 ">
+            <BookmarkFolderNameInput
+              onClickOutside={() => setIsRenameInputShown(false)}
+              onPressEnter={onPressEnterHandler}
             />
           </div>
         )

+ 92 - 30
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolderItem.tsx

@@ -4,51 +4,101 @@ import {
 
 import { useTranslation } from 'next-i18next';
 
+import { toastError, toastSuccess } from '~/client/util/apiNotification';
+import { apiv3Post } from '~/client/util/apiv3-client';
 import CountBadge from '~/components/Common/CountBadge';
 import FolderIcon from '~/components/Icons/FolderIcon';
 import TriangleIcon from '~/components/Icons/TriangleIcon';
 import { BookmarkFolderItems } from '~/server/models/bookmark-folder';
 import { useSWRxBookamrkFolderAndChild } from '~/stores/bookmark-folder';
 
+import BookmarkFolderNameInput from './BookmarkFolderNameInput';
+
 
 type BookmarkFolderItemProps = {
   bookmarkFolder: BookmarkFolderItems
   isOpen?: boolean
 }
 const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderItemProps) => {
-  const {
-    bookmarkFolder, isOpen: _isOpen = false,
-  } = props;
+  const { bookmarkFolder, isOpen: _isOpen = false } = props;
+
   const { t } = useTranslation();
-  const [currentParentFolder, setCurrentParentFolder] = useState<string | null>(null);
-  const [isOpen, setIsOpen] = useState(_isOpen);
-  const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(isOpen, currentParentFolder);
   const { name, _id: parentId, children } = bookmarkFolder;
+  const [currentChildren, setCurrentChildren] = useState<BookmarkFolderItems[]>();
+  const [isRenameInputShown, setIsRenameInputShown] = useState<boolean>(false);
+  const [currentParentFolder, setCurrentParentFolder] = useState<string | null>(parentId);
+  const [isOpen, setIsOpen] = useState(_isOpen);
+  const { data: childBookmarkFolderData, mutate: mutateChildBookmarkData } = useSWRxBookamrkFolderAndChild(isOpen ? currentParentFolder : null);
 
-  const hasChildren = useCallback((): boolean => {
-    if (children != null) {
-      return children.length > 0;
+
+  const childCount = useCallback((): number => {
+    if (currentChildren != null && currentChildren.length > children.length) {
+      return currentChildren.length;
     }
-    return false;
-  }, [children]);
+    return children.length;
+  }, [children.length, currentChildren]);
 
   useEffect(() => {
-    setCurrentParentFolder(parentId);
-  }, [bookmarkFolder, parentId]);
+    if (isOpen && childBookmarkFolderData != null) {
+      mutateChildBookmarkData();
+      setCurrentChildren(childBookmarkFolderData);
+    }
+  }, [childBookmarkFolderData, isOpen, mutateChildBookmarkData]);
+
+  const hasChildren = useCallback((): boolean => {
+    if (currentChildren != null && currentChildren.length > children.length) {
+      return currentChildren.length > 0;
+    }
+    return children.length > 0;
+  }, [children.length, currentChildren]);
+
 
   const loadChildFolder = useCallback(async() => {
-    setCurrentParentFolder(parentId);
     setIsOpen(!isOpen);
-    mutateChildBookmarkData();
-  }, [parentId, isOpen, mutateChildBookmarkData]);
+    setCurrentParentFolder(bookmarkFolder._id);
+  }, [bookmarkFolder, isOpen]);
+
+
+  const onPressEnterHandler = useCallback(async(folderName: string) => {
+
+    try {
+      await apiv3Post('/bookmark-folder', { name: folderName, parent: currentParentFolder });
+      setIsOpen(true);
+      setIsRenameInputShown(false);
+      mutateChildBookmarkData();
+      toastSuccess(t('Create New Bookmark Folder Success'));
+    }
+    catch (err) {
+      toastError(err);
+    }
+
+  }, [currentParentFolder, mutateChildBookmarkData, t]);
+
+  const onClickPlusButton = useCallback(async() => {
+    if (!isOpen && hasChildren()) {
+      setIsOpen(true);
+    }
+    setIsRenameInputShown(true);
+  }, [hasChildren, isOpen]);
+
+  const renderChildFolder = () => {
+    return isOpen && currentChildren?.map((childFolder) => {
+      return (
+        <div key={childFolder._id} className="grw-foldertree-item-children">
+          <BookmarkFolderItem
+            key={childFolder._id}
+            bookmarkFolder={childFolder}
+          />
+        </div>
+      );
+    });
+  };
 
 
   return (
     <div id={`bookmark-folder-item-${bookmarkFolder._id}`} 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 className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center">
         <div className="grw-triangle-container d-flex justify-content-center">
           {hasChildren() && (
             <button
@@ -68,27 +118,39 @@ const BookmarkFolderItem: FC<BookmarkFolderItemProps> = (props: BookmarkFolderIt
           </div>
         }
         {
-          <div className='grw-foldertree-title-anchor flex-grow-1 pl-2'>
+          <div className='grw-foldertree-title-anchor flex-grow-1 pl-2' onClick={loadChildFolder}>
             <p className={'text-truncate m-auto '}>{name}</p>
           </div>
         }
         {hasChildren() && (
           <div className="grw-foldertree-count-wrapper">
-            <CountBadge count={ children.length} />
+            <CountBadge count={ childCount() } />
           </div>
         )}
+        <div className="grw-foldertree-control d-flex">
+
+
+          <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>
+      {isRenameInputShown && (
+        <div className="flex-fill">
+          <BookmarkFolderNameInput
+            onClickOutside={() => setIsRenameInputShown(false)}
+            onPressEnter={onPressEnterHandler}
+          />
+        </div>
+      )}
       {
-        isOpen && hasChildren() && childBookmarkFolderData?.map(children => (
-          <div key={children._id} className="grw-foldertree-item-children">
-            <BookmarkFolderItem
-              key={children._id}
-              bookmarkFolder={children}
-              isOpen = {false}
-            />
-          </div>
-        ))
+        renderChildFolder()
       }
     </div>
   );

+ 43 - 0
packages/app/src/components/Sidebar/Bookmarks/BookmarkFolderNameInput.tsx

@@ -0,0 +1,43 @@
+import { useTranslation } from 'next-i18next';
+
+import ClosableTextInput, { AlertInfo, AlertType } from '~/components/Common/ClosableTextInput';
+
+
+type Props = {
+  onClickOutside: () => void
+  onPressEnter: (folderName: string) => void
+  value?: string
+}
+
+const BookmarkFolderNameInput = (props: Props): JSX.Element => {
+  const {
+    onClickOutside, onPressEnter, value,
+  } = props;
+  const { t } = useTranslation();
+
+
+  const inputValidator = (title: string | null): AlertInfo | null => {
+    if (title == null || title === '' || title.trim() === '') {
+      return {
+        type: AlertType.WARNING,
+        message: t('form_validation.title_required'),
+      };
+    }
+    return null;
+  };
+
+  return (
+    <div className="flex-fill">
+      <ClosableTextInput
+        value={ value }
+        placeholder={t('Input Folder name')}
+        onClickOutside={onClickOutside}
+        onPressEnter={onPressEnter}
+        inputValidator={inputValidator}
+      />
+    </div>
+  );
+};
+
+
+export default BookmarkFolderNameInput;

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

@@ -1,4 +1,3 @@
-import React from 'react';
 
 import { useTranslation } from 'next-i18next';
 
@@ -11,10 +10,10 @@ import styles from './BookmarkFolderTree.module.scss';
 
 const BookmarkFolderTree = (): JSX.Element => {
   const { t } = useTranslation();
-  const { data: bookmarkFolderData } = useSWRxBookamrkFolderAndChild(true);
-
+  const { data: bookmarkFolderData } = useSWRxBookamrkFolderAndChild();
   if (bookmarkFolderData != null) {
     return (
+
       <ul className={`grw-foldertree ${styles['grw-foldertree']} list-group p-3`}>
         {bookmarkFolderData.map((item) => {
           return (

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

@@ -5,10 +5,10 @@ import useSWRImmutable from 'swr/immutable';
 import { apiv3Get } from '~/client/util/apiv3-client';
 import { BookmarkFolderItems } from '~/server/models/bookmark-folder';
 
-export const useSWRxBookamrkFolderAndChild = (isOpen: boolean, parentId?: Nullable<string>): SWRResponse<BookmarkFolderItems[], Error> => {
-  const _parentId = parentId ?? '';
+export const useSWRxBookamrkFolderAndChild = (parentId?: Nullable<string>): SWRResponse<BookmarkFolderItems[], Error> => {
+  const _parentId = parentId == null ? '' : parentId;
   return useSWRImmutable(
-    isOpen ? `/bookmark-folder/list/${_parentId}` : null,
+    `/bookmark-folder/list/${_parentId}`,
     endpoint => apiv3Get(endpoint).then((response) => {
       return response.data.bookmarkFolderItems;
     }),