Parcourir la source

Merge branch 'feat/gw7840-implement-rename-from-bookmark-sidebar' into feat/gw7841-implement-delete-from-bookmark-sidebar

Mudana-Grune il y a 3 ans
Parent
commit
203fc6f379

+ 4 - 2
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -7,7 +7,7 @@ import {
 import { useIsGuestUser } from '~/stores/context';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 
-import { useSWRBookmarkInfo } from '../../stores/bookmark';
+import { useSWRBookmarkInfo, useSWRxCurrentUserBookmarks } from '../../stores/bookmark';
 import { useSWRxPageInfo } from '../../stores/page';
 import { useSWRxUsersList } from '../../stores/user';
 import BookmarkButtons from '../BookmarkButtons';
@@ -51,6 +51,7 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
   const { mutate: mutatePageInfo } = useSWRxPageInfo(pageId, shareLinkId);
 
   const { data: bookmarkInfo, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(pageId);
+  const { mutate: mutateCurrentUserBookmark } = useSWRxCurrentUserBookmarks();
 
   const likerIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.likerIds ?? []).slice(0, 15) : [];
   const seenUserIds = isIPageInfoForEntity(pageInfo) ? (pageInfo.seenUserIds ?? []).slice(0, 15) : [];
@@ -95,7 +96,8 @@ const SubNavButtonsSubstance = (props: SubNavButtonsSubstanceProps): JSX.Element
     await toggleBookmark(pageId, pageInfo.isBookmarked);
     mutatePageInfo();
     mutateBookmarkInfo();
-  }, [isGuestUser, mutateBookmarkInfo, mutatePageInfo, pageId, pageInfo]);
+    mutateCurrentUserBookmark();
+  }, [isGuestUser, mutateBookmarkInfo, mutatePageInfo, mutateCurrentUserBookmark, pageId, pageInfo]);
 
   const duplicateMenuItemClickHandler = useCallback(async(_pageId: string): Promise<void> => {
     if (onClickDuplicateMenuItem == null || path == null) {

+ 3 - 1
packages/app/src/components/PageList/PageListItemL.tsx

@@ -23,6 +23,7 @@ import {
   OnDuplicatedFunction, OnRenamedFunction, OnDeletedFunction, OnPutBackedFunction,
 } from '~/interfaces/ui';
 import LinkedPagePath from '~/models/linked-page-path';
+import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import {
   usePageRenameModal, usePageDuplicateModal, usePageDeleteModal, usePutBackPageModal,
 } from '~/stores/modal';
@@ -85,7 +86,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
 
   const shouldFetch = isSelected && (pageData != null || pageMeta != null);
   const { data: pageInfo } = useSWRxPageInfo(shouldFetch ? pageData?._id : null);
-
+  const { mutate: mutateCurrentUserBookmark } = useSWRxCurrentUserBookmarks();
   const elasticSearchResult = isIPageSearchMeta(pageMeta) ? pageMeta.elasticSearchResult : null;
   const revisionShortBody = isIPageInfoForListing(pageMeta) ? pageMeta.revisionShortBody : null;
 
@@ -122,6 +123,7 @@ const PageListItemLSubstance: ForwardRefRenderFunction<ISelectable, Props> = (pr
   const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
     const bookmarkOperation = _newValue ? bookmark : unbookmark;
     await bookmarkOperation(_pageId);
+    mutateCurrentUserBookmark();
   };
 
   const duplicateMenuItemClickHandler = useCallback(() => {

+ 81 - 112
packages/app/src/components/Sidebar/Bookmarks.tsx

@@ -1,5 +1,5 @@
 
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback, useState } from 'react';
 
 import nodePath from 'path';
 
@@ -9,38 +9,37 @@ import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 
 import { unbookmark } from '~/client/services/page-operation';
 import { toastError, toastSuccess } from '~/client/util/apiNotification';
-import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+import { apiv3Put } from '~/client/util/apiv3-client';
 import { IPageHasId, IPageInfoAll, IPageToDeleteWithMeta } from '~/interfaces/page';
 import { OnDeletedFunction } from '~/interfaces/ui';
-import { useCurrentUser, useIsGuestUser } from '~/stores/context';
+import { useIsGuestUser } from '~/stores/context';
 import { usePageDeleteModal } from '~/stores/modal';
-import loggerFactory from '~/utils/logger';
+
+import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 
 import ClosableTextInput, { AlertInfo, AlertType } from '../Common/ClosableTextInput';
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
 
-const logger = loggerFactory('growi:BookmarkList');
-// TODO: Remove pagination and apply  scrolling (not infinity)
-const ACTIVE_PAGE = 1;
 
 type Props = {
-  page: IPageHasId,
-  refreshBookmarkList: () => void
+  bookmarkedPage: IPageHasId,
+  onPageOperationSuccess: () => void
 }
 
-const BookmarksItem = (props: Props) => {
+const BookmarkItem = (props: Props) => {
+  const { bookmarkedPage, onPageOperationSuccess } = props;
   const { t } = useTranslation();
-  const { page, refreshBookmarkList } = props;
   const [isRenameInputShown, setRenameInputShown] = useState(false);
-  const dPagePath = new DevidedPagePath(page.path, false, true);
-  const { latter: pageTitle, former: formerPagePath } = dPagePath;
-  const bookmarkItemId = `bookmark-item-${page._id}`;
+  const dPagePath = new DevidedPagePath(bookmarkedPage.path, false, true);
+  const { latter: pageTitle, former, isRoot } = dPagePath;
+  const formerPagePath = isRoot ? pageTitle : pathUtils.addTrailingSlash(former);
+  const bookmarkItemId = `bookmark-item-${bookmarkedPage._id}`;
   const { open: openDeleteModal } = usePageDeleteModal();
 
   const bookmarkMenuItemClickHandler = useCallback(async() => {
-    await unbookmark(page._id);
-    refreshBookmarkList();
-  }, [page, refreshBookmarkList]);
+    await unbookmark(bookmarkedPage._id);
+    onPageOperationSuccess();
+  }, [onPageOperationSuccess, bookmarkedPage]);
 
   const renameMenuItemClickHandler = useCallback(() => {
     setRenameInputShown(true);
@@ -57,10 +56,10 @@ const BookmarksItem = (props: Props) => {
     return null;
   };
 
-  const pressEnterForRenameHandler = (async(inputText: string) => {
-    const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(page.path ?? ''));
+  const pressEnterForRenameHandler = useCallback(async(inputText: string) => {
+    const parentPath = pathUtils.addTrailingSlash(nodePath.dirname(bookmarkedPage.path ?? ''));
     const newPagePath = nodePath.resolve(parentPath, inputText);
-    if (newPagePath === page.path) {
+    if (newPagePath === bookmarkedPage.path) {
       setRenameInputShown(false);
       return;
     }
@@ -68,18 +67,18 @@ const BookmarksItem = (props: Props) => {
     try {
       setRenameInputShown(false);
       await apiv3Put('/pages/rename', {
-        pageId: page._id,
-        revisionId: page.revision,
+        pageId: bookmarkedPage._id,
+        revisionId: bookmarkedPage.revision,
         newPagePath,
       });
-      refreshBookmarkList();
-      toastSuccess(t('renamed_pages', { path: page.path }));
+      onPageOperationSuccess();
+      toastSuccess(t('renamed_pages', { path: bookmarkedPage.path }));
     }
     catch (err) {
       setRenameInputShown(true);
       toastError(err);
     }
-  });
+  }, [bookmarkedPage, onPageOperationSuccess, t]);
 
   const deleteMenuItemClickHandler = useCallback(async(_pageId: string, pageInfo: IPageInfoAll | undefined): Promise<void> => {
     const onClickDeleteMenuItem = (pageToDelete: IPageToDeleteWithMeta) => {
@@ -95,105 +94,88 @@ const BookmarksItem = (props: Props) => {
         else {
           toastSuccess(t('deleted_pages', { path }));
         }
-        refreshBookmarkList();
+        onPageOperationSuccess();
       };
       openDeleteModal([pageToDelete], { onDeleted: onDeletedHandler });
     };
 
-    if (page._id == null || page.path == null) {
+    if (bookmarkedPage._id == null || bookmarkedPage.path == null) {
       throw Error('_id and path must not be null.');
     }
 
     const pageToDelete: IPageToDeleteWithMeta = {
       data: {
-        _id: page._id,
-        revision: page.revision as string,
-        path: page.path,
+        _id: bookmarkedPage._id,
+        revision: bookmarkedPage.revision as string,
+        path: bookmarkedPage.path,
       },
       meta: pageInfo,
     };
 
     onClickDeleteMenuItem(pageToDelete);
-  }, [page, openDeleteModal, refreshBookmarkList, t]);
+  }, [bookmarkedPage, openDeleteModal, onPageOperationSuccess, t]);
 
   return (
-    <>
-      <div className="d-flex justify-content-between" key={page._id}>
-        <li className="list-group-item list-group-item-action border-0 py-0 pr-3 d-flex align-items-center" id={bookmarkItemId}>
-          { isRenameInputShown ? (
-            <ClosableTextInput
-              value={nodePath.basename(page.path ?? '')}
-              placeholder={t('Input page name')}
-              onClickOutside={() => { setRenameInputShown(false) }}
-              onPressEnter={pressEnterForRenameHandler}
-              inputValidator={inputValidator}
-            />
-          ) : (
-            <a href={`/${page._id}`} className="grw-bookmarks-title-anchor flex-grow-1">
-              <p className={`text-truncate m-auto ${page.isEmpty && 'grw-sidebar-text-muted'}`}>{pageTitle}</p>
-            </a>
-          )}
-          <PageItemControl
-            pageId={page._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}
-          >
-            { formerPagePath || '/' }
-          </UncontrolledTooltip>
-        </li>
-      </div>
-    </>
+    <div className="d-flex justify-content-between" key={bookmarkedPage._id}>
+      <li className="list-group-item list-group-item-action border-0 py-0 pr-3 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-bookmarks-title-anchor flex-grow-1">
+            <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 }
+        </UncontrolledTooltip>
+      </li>
+    </div>
   );
 };
 
-
 const Bookmarks = () : JSX.Element => {
   const { t } = useTranslation();
-  const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
-  const [pages, setPages] = useState<IPageHasId[]>([]);
-  const page = ACTIVE_PAGE;
+  const { data: currentUserBookmarksData, mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
 
-  const getMyBookmarkList = useCallback(async() => {
-    try {
-      const res = await apiv3Get(`/bookmarks/${currentUser?._id}`, { page });
-      const { paginationResult } = res.data;
-      setPages(paginationResult.docs.map((page) => {
-        return {
-          ...page.page,
-        };
-      }));
-    }
-    catch (error) {
-      logger.error('failed to fetch data', error);
-      toastError(error, 'Error occurred in bookmark page list');
+  const renderBookmarkList = () => {
+    if (currentUserBookmarksData?.length === 0) {
+      return (
+        <h4 className="pl-3">
+          { t('No bookmarks yet') }
+        </h4>
+      );
     }
-  }, [currentUser, page]);
-
-  useEffect(() => {
-    getMyBookmarkList();
-  }, [getMyBookmarkList]);
-
-  const generateBookmarkList = () => {
     return (
       <ul className="grw-bookmarks-list list-group p-3">
         <div className="grw-bookmarks-item-container">
-          { pages.map((page) => {
+          { currentUserBookmarksData?.map((currentUserBookmark) => {
             return (
-              <BookmarksItem key={page._id} page={page} refreshBookmarkList={getMyBookmarkList} />
+              <BookmarkItem key={currentUserBookmark._id} bookmarkedPage={currentUserBookmark} onPageOperationSuccess={mutateCurrentUserBookmarks} />
             );
           })}
         </div>
@@ -201,17 +183,6 @@ const Bookmarks = () : JSX.Element => {
     );
   };
 
-  const renderBookmarksItem = () => {
-    if (pages?.length === 0) {
-      return (
-        <h3 className="pl-3">
-          { t('No bookmarks yet') }
-        </h3>
-      );
-    }
-    return generateBookmarkList();
-  };
-
   return (
     <>
       <div className="grw-sidebar-content-header p-3">
@@ -219,15 +190,13 @@ const Bookmarks = () : JSX.Element => {
       </div>
       { isGuestUser
         ? (
-          <h3 className="pl-3">
+          <h4 className="pl-3">
             { t('Not available for guest') }
-          </h3>
-        ) : renderBookmarksItem()
+          </h4>
+        ) : renderBookmarkList()
       }
-
     </>
   );
-
 };
 
 export default Bookmarks;

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

@@ -16,6 +16,7 @@ import TriangleIcon from '~/components/Icons/TriangleIcon';
 import {
   IPageHasId, IPageInfoAll, IPageToDeleteWithMeta,
 } from '~/interfaces/page';
+import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
 import { IPageForPageDuplicateModal } from '~/stores/modal';
 import { useSWRxPageChildren } from '~/stores/page-listing';
 import { usePageTreeDescCountMap } from '~/stores/ui';
@@ -57,12 +58,6 @@ const markTarget = (children: ItemNode[], targetPathOrId?: string): void => {
   });
 };
 
-
-const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
-  const bookmarkOperation = _newValue ? bookmark : unbookmark;
-  await bookmarkOperation(_pageId);
-};
-
 /**
  * Return new page path after the droppedPagePath is moved under the newParentPagePath
  * @param droppedPagePath
@@ -111,6 +106,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   const [isCreating, setCreating] = useState(false);
 
   const { data, mutate: mutateChildren } = useSWRxPageChildren(isOpen ? page._id : null);
+  const { mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
 
   // descendantCount
   const { getDescCount } = usePageTreeDescCountMap();
@@ -238,6 +234,12 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     }
   }, [hasDescendants]);
 
+  const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
+    const bookmarkOperation = _newValue ? bookmark : unbookmark;
+    await bookmarkOperation(_pageId);
+    mutateCurrentUserBookmarks();
+  };
+
   const duplicateMenuItemClickHandler = useCallback((): void => {
     if (onClickDuplicateMenuItem == null) {
       return;

+ 0 - 2
packages/app/src/server/routes/apiv3/bookmarks.js

@@ -201,7 +201,6 @@ module.exports = (crowi) => {
     const { userId } = req.params;
     const page = req.query.page;
     const limit = parseInt(req.query.limit) || await crowi.configManager.getConfig('crowi', 'customize:showPageLimitationM') || 30;
-    const offset = page > 0 ? (page - 1) * limit : page;
 
     if (userId == null) {
       return res.apiv3Err('User id is not found or forbidden', 400);
@@ -223,7 +222,6 @@ module.exports = (crowi) => {
               model: 'User',
             },
           },
-          offset,
           page,
           limit,
         },

+ 20 - 0
packages/app/src/stores/bookmark.ts

@@ -1,9 +1,13 @@
 import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
+import { Nullable } from '~/interfaces/common';
+import { IPageHasId } from '~/interfaces/page';
+
 import { apiv3Get } from '../client/util/apiv3-client';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
 
+import { useCurrentUser } from './context';
 
 export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRResponse<IBookmarkInfo, Error> => {
   return useSWRImmutable(
@@ -17,3 +21,19 @@ export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRRespon
     }),
   );
 };
+
+export const useSWRxCurrentUserBookmarks = (pageNum?: Nullable<number>): SWRResponse<IPageHasId[], Error> => {
+  const { data: currentUser } = useCurrentUser();
+  const currentPage = pageNum ?? 1;
+  return useSWRImmutable(
+    currentUser != null ? `/bookmarks/${currentUser._id}` : null,
+    endpoint => apiv3Get(endpoint, { page: currentPage }).then((response) => {
+      const { paginationResult } = response.data;
+      return paginationResult.docs.map((item) => {
+        return {
+          ...item.page,
+        };
+      });
+    }),
+  );
+};

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

@@ -20,7 +20,6 @@ $bgcolor-page-list-group-item-active: lighten($primary, 76%) !default;
 $color-page-list-group-item-meta: $gray-500 !default;
 $color-search-page-list-title: $color-global !default;
 $bgcolor-subnav: darken($bgcolor-global, 3%) !default;
-$bgcolor-list-hover: darken($primary, 8%) !default;
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;