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

Merge pull request #6346 from weseek/feat/gw7839-remove-bookmark

feat: gw7839 Implement remove bookmark
cao 3 лет назад
Родитель
Сommit
f75a79e180

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

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

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

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

+ 61 - 90
packages/app/src/components/Sidebar/Bookmarks.tsx

@@ -1,116 +1,89 @@
 
 
-import React, { useCallback, useEffect, useState } from 'react';
+import React, { useCallback } from 'react';
 
 
-import { DevidedPagePath } from '@growi/core';
+import { DevidedPagePath, pathUtils } from '@growi/core';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 import { UncontrolledTooltip, DropdownToggle } from 'reactstrap';
 
 
-import { toastError } from '~/client/util/apiNotification';
-import { apiv3Get } from '~/client/util/apiv3-client';
+import { unbookmark } from '~/client/services/page-operation';
 import { IPageHasId } from '~/interfaces/page';
 import { IPageHasId } from '~/interfaces/page';
-import { useCurrentUser, useIsGuestUser } from '~/stores/context';
-import loggerFactory from '~/utils/logger';
-
+import { useSWRxCurrentUserBookmarks } from '~/stores/bookmark';
+import { useIsGuestUser } from '~/stores/context';
 
 
 import { MenuItemType, PageItemControl } from '../Common/Dropdown/PageItemControl';
 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 = {
 type Props = {
-  pages: IPageHasId[]
+  bookmarkedPage: IPageHasId,
+  onPageOperationSuccess: () => void
 }
 }
 
 
-const BookmarksItem = (props: Props) => {
-  const { pages } = props;
+const BookmarkItem = (props: Props) => {
+  const { bookmarkedPage, onPageOperationSuccess } = props;
 
 
-  const generateBookmarkedPageList = pages.map((page) => {
-    const dPagePath = new DevidedPagePath(page.path, false, true);
-    const { latter: pageTitle, former: formerPagePath } = dPagePath;
-    const bookmarkItemId = `bookmark-item-${page._id}`;
-
-    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}>
-          <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]}
-          >
-            <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 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 bookmarkMenuItemClickHandler = useCallback(async() => {
+    await unbookmark(bookmarkedPage._id);
+    onPageOperationSuccess();
+  }, [onPageOperationSuccess, bookmarkedPage]);
 
 
   return (
   return (
-    <>
-      <ul className="grw-bookmarks-list list-group p-3">
-        <div className="grw-bookmarks-item-container">
-          {generateBookmarkedPageList}
-        </div>
-      </ul>
-    </>
+    <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}>
+        <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}
+        >
+          <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>
   );
   );
 };
 };
 
 
-
 const Bookmarks = () : JSX.Element => {
 const Bookmarks = () : JSX.Element => {
   const { t } = useTranslation();
   const { t } = useTranslation();
-  const { data: currentUser } = useCurrentUser();
   const { data: isGuestUser } = useIsGuestUser();
   const { data: isGuestUser } = useIsGuestUser();
-  const [pages, setPages] = useState<IPageHasId[]>([]);
+  const { data: currentUserBookmarksData, mutate: mutateCurrentUserBookmarks } = useSWRxCurrentUserBookmarks();
 
 
-  const getMyBookmarkList = useCallback(async() => {
-    // TODO: Remove pagination and apply  scrolling (not infinity)
-    const page = ACTIVE_PAGE;
-
-    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');
-    }
-  }, [currentUser]);
-
-  useEffect(() => {
-    getMyBookmarkList();
-  }, [getMyBookmarkList]);
-
-  const renderBookmarksItem = () => {
-    if (pages.length === 0) {
+  const renderBookmarkList = () => {
+    if (currentUserBookmarksData?.length === 0) {
       return (
       return (
-        <h3 className="pl-3">
+        <h4 className="pl-3">
           { t('No bookmarks yet') }
           { t('No bookmarks yet') }
-        </h3>
+        </h4>
       );
       );
     }
     }
-    return <BookmarksItem pages={pages} />;
+    return (
+      <ul className="grw-bookmarks-list list-group p-3">
+        <div className="grw-bookmarks-item-container">
+          { currentUserBookmarksData?.map((currentUserBookmark) => {
+            return (
+              <BookmarkItem key={currentUserBookmark._id} bookmarkedPage={currentUserBookmark} onPageOperationSuccess={mutateCurrentUserBookmarks} />
+            );
+          })}
+        </div>
+      </ul>
+    );
   };
   };
 
 
   return (
   return (
@@ -120,15 +93,13 @@ const Bookmarks = () : JSX.Element => {
       </div>
       </div>
       { isGuestUser
       { isGuestUser
         ? (
         ? (
-          <h3 className="pl-3">
+          <h4 className="pl-3">
             { t('Not available for guest') }
             { t('Not available for guest') }
-          </h3>
-        ) : renderBookmarksItem()
+          </h4>
+        ) : renderBookmarkList()
       }
       }
-
     </>
     </>
   );
   );
-
 };
 };
 
 
 export default Bookmarks;
 export default Bookmarks;

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

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

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

@@ -1,9 +1,13 @@
 import { SWRResponse } from 'swr';
 import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 import useSWRImmutable from 'swr/immutable';
 
 
+import { Nullable } from '~/interfaces/common';
+import { IPageHasId } from '~/interfaces/page';
+
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
 import { IBookmarkInfo } from '../interfaces/bookmark-info';
 
 
+import { useCurrentUser } from './context';
 
 
 export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRResponse<IBookmarkInfo, Error> => {
 export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRResponse<IBookmarkInfo, Error> => {
   return useSWRImmutable(
   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,
+        };
+      });
+    }),
+  );
+};