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

impl bookmark operation for PageListItem

Yuki Takei 4 лет назад
Родитель
Сommit
ad704e8ee6

+ 20 - 14
packages/app/src/components/BookmarkButtons.tsx

@@ -9,16 +9,20 @@ import UserPictureList from './User/UserPictureList';
 import { useIsGuestUser } from '~/stores/context';
 
 interface Props {
+  bookmarkCount: number
+  isBookmarked?: boolean
+  bookmarkedUsers?: IUser[]
   hideTotalNumber?: boolean
-  isBookmarked: boolean
-  sumOfBookmarks: number
-  bookmarkedUsers: IUser[]
   onBookMarkClicked: ()=>void;
 }
 
 const BookmarkButtons: FC<Props> = (props: Props) => {
   const { t } = useTranslation();
 
+  const {
+    bookmarkCount, isBookmarked, bookmarkedUsers, hideTotalNumber,
+  } = props;
+
   const [isPopoverOpen, setIsPopoverOpen] = useState(false);
 
   const { data: isGuestUser } = useIsGuestUser();
@@ -40,9 +44,9 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         id="bookmark-button"
         onClick={handleClick}
         className={`btn btn-bookmark border-0
-          ${props.isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
       >
-        <i className={`fa ${props.isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
+        <i className={`fa ${isBookmarked ? 'fa-bookmark' : 'fa-bookmark-o'}`}></i>
       </button>
 
       {isGuestUser && (
@@ -51,18 +55,20 @@ const BookmarkButtons: FC<Props> = (props: Props) => {
         </UncontrolledTooltip>
       )}
 
-      { !props.hideTotalNumber && (
+      { !hideTotalNumber && (
         <>
           <button type="button" id="po-total-bookmarks" className={`btn btn-bookmark border-0 total-bookmarks ${props.isBookmarked ? 'active' : ''}`}>
-            {props.sumOfBookmarks}
+            {bookmarkCount}
           </button>
-          <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-bookmarks" toggle={togglePopover} trigger="legacy">
-            <PopoverBody className="user-list-popover">
-              <div className="px-2 text-right user-list-content text-truncate text-muted">
-                {props.bookmarkedUsers.length ? <UserPictureList users={props.bookmarkedUsers} /> : t('No users have bookmarked yet')}
-              </div>
-            </PopoverBody>
-          </Popover>
+          { bookmarkedUsers != null && (
+            <Popover placement="bottom" isOpen={isPopoverOpen} target="po-total-bookmarks" toggle={togglePopover} trigger="legacy">
+              <PopoverBody className="user-list-popover">
+                <div className="px-2 text-right user-list-content text-truncate text-muted">
+                  {bookmarkedUsers.length ? <UserPictureList users={props.bookmarkedUsers} /> : t('No users have bookmarked yet')}
+                </div>
+              </PopoverBody>
+            </Popover>
+          ) }
         </>
       ) }
     </div>

+ 29 - 34
packages/app/src/components/Common/Dropdown/PageItemControl.tsx

@@ -1,4 +1,4 @@
-import React, { FC, useState, useCallback } from 'react';
+import React, { useState, useCallback } from 'react';
 import {
   Dropdown, DropdownMenu, DropdownToggle, DropdownItem,
 } from 'reactstrap';
@@ -9,11 +9,8 @@ import { useTranslation } from 'react-i18next';
 import loggerFactory from '~/utils/logger';
 
 import {
-  IPageHasId, IPageInfo, IPageInfoCommon, isExistPageInfo,
+  IPageInfo, IPageInfoCommon, isExistPageInfo,
 } from '~/interfaces/page';
-import { apiv3Put } from '~/client/util/apiv3-client';
-import { toastError } from '~/client/util/apiNotification';
-import { useSWRBookmarkInfo } from '~/stores/bookmark';
 import { useSWRxPageInfo } from '~/stores/page';
 
 const logger = loggerFactory('growi:cli:PageItemControl');
@@ -21,9 +18,9 @@ const logger = loggerFactory('growi:cli:PageItemControl');
 type CommonProps = {
   pageInfo?: IPageInfoCommon | IPageInfo,
   isEnableActions?: boolean
-  onClickBookmarkMenuItem?: (pageId: string, bool: boolean) => void
-  onClickRenameMenuItem?: (pageId: string) => void
-  onClickDeleteMenuItem?: (pageId: string) => void
+  onClickBookmarkMenuItem?: (pageId: string, newValue?: boolean) => Promise<void>
+  onClickRenameMenuItem?: (pageId: string) => Promise<void>
+  onClickDeleteMenuItem?: (pageId: string) => Promise<void>
 }
 
 
@@ -40,47 +37,36 @@ const PageItemControlDropdownMenu = React.memo((props: DropdownMenuProps): JSX.E
   } = props;
 
 
-  const isEmpty = isExistPageInfo(pageInfo);
+  const isEmpty = !isExistPageInfo(pageInfo);
 
   // eslint-disable-next-line react-hooks/rules-of-hooks
-  const bookmarkItemClickedHandler = useCallback(() => {
-    if (!isEmpty || onClickBookmarkMenuItem == null) {
+  const bookmarkItemClickedHandler = useCallback(async() => {
+    if (isEmpty || onClickBookmarkMenuItem == null) {
       return;
     }
-    onClickBookmarkMenuItem(pageId, !pageInfo.isBookmarked);
+    await onClickBookmarkMenuItem(pageId, !pageInfo.isBookmarked);
   }, [isEmpty, onClickBookmarkMenuItem, pageId, pageInfo]);
 
   // eslint-disable-next-line react-hooks/rules-of-hooks
-  const renameItemClickedHandler = useCallback(() => {
+  const renameItemClickedHandler = useCallback(async() => {
     if (onClickRenameMenuItem == null) {
       return;
     }
-    onClickRenameMenuItem(pageId);
+    await onClickRenameMenuItem(pageId);
   }, [onClickRenameMenuItem, pageId]);
 
   // eslint-disable-next-line react-hooks/rules-of-hooks
-  const deleteItemClickedHandler = useCallback(() => {
-    if (!isEmpty || onClickDeleteMenuItem == null) {
+  const deleteItemClickedHandler = useCallback(async() => {
+    if (isEmpty || onClickDeleteMenuItem == null) {
       return;
     }
     if (!pageInfo.isDeletable) {
       logger.warn('This page could not be deleted.');
       return;
     }
-    onClickDeleteMenuItem(pageId);
+    await onClickDeleteMenuItem(pageId);
   }, [isEmpty, onClickDeleteMenuItem, pageId, pageInfo]);
 
-  // const bookmarkToggleHandler = (async() => {
-  //   try {
-  //     // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
-  //     await apiv3Put('/bookmarks', { pageId: page._id, bool: !bookmarkInfo!.isBookmarked });
-  //     mutateBookmarkInfo();
-  //   }
-  //   catch (err) {
-  //     toastError(err);
-  //   }
-  // });
-
   if (pageId == null || pageInfo == null) {
     return <></>;
   }
@@ -139,13 +125,24 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
 
   const {
     pageId, pageInfo: presetPageInfo, fetchOnOpen,
-    onClickBookmarkMenuItem, onClickRenameMenuItem, onClickDeleteMenuItem,
+    onClickBookmarkMenuItem,
   } = props;
 
   const [isOpen, setIsOpen] = useState(false);
 
   const shouldFetch = presetPageInfo == null && (!fetchOnOpen || isOpen);
-  const { data: fetchedPageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
+  const { data: fetchedPageInfo, mutate: mutatePageInfo } = useSWRxPageInfo(shouldFetch ? pageId : null);
+
+  // mutate after handle event
+  const bookmarkMenuItemClickHandler = useCallback(async(_pageId: string, _newValue: boolean) => {
+    if (onClickBookmarkMenuItem != null) {
+      await onClickBookmarkMenuItem(_pageId, _newValue);
+    }
+
+    if (shouldFetch) {
+      mutatePageInfo();
+    }
+  }, [mutatePageInfo, onClickBookmarkMenuItem, shouldFetch]);
 
   return (
     <Dropdown isOpen={isOpen} toggle={() => setIsOpen(!isOpen)}>
@@ -154,11 +151,9 @@ export const PageItemControlSubstance = (props: PageItemControlSubstanceProps):
       </DropdownToggle>
 
       <PageItemControlDropdownMenu
-        pageId={pageId}
+        {...props}
         pageInfo={presetPageInfo ?? fetchedPageInfo}
-        onClickBookmarkMenuItem={onClickBookmarkMenuItem}
-        onClickRenameMenuItem={onClickRenameMenuItem}
-        onClickDeleteMenuItem={onClickDeleteMenuItem}
+        onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
       />
     </Dropdown>
   );

+ 5 - 6
packages/app/src/components/PageList/PageListItemL.tsx

@@ -3,15 +3,13 @@ import React, { FC, memo, useCallback } from 'react';
 import Clamp from 'react-multiline-clamp';
 
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
-import { pagePathUtils, DevidedPagePath } from '@growi/core';
+import { DevidedPagePath } from '@growi/core';
 import { useIsDeviceSmallerThanLg } from '~/stores/ui';
 import { IPageWithMeta } from '~/interfaces/page';
 import { IPageSearchMeta, isIPageSearchMeta } from '~/interfaces/search';
 
 import { AsyncPageItemControl } from '../Common/Dropdown/PageItemControl';
 
-const { isTopPage, isUserNamePage } = pagePathUtils;
-
 type Props = {
   page: IPageWithMeta | IPageWithMeta<IPageSearchMeta>,
   isSelected?: boolean, // is item selected(focused)
@@ -19,9 +17,9 @@ type Props = {
   isEnableActions?: boolean,
   shortBody?: string
   showPageUpdatedTime?: boolean, // whether to show page's updated time at the top-right corner of item
-  onClickCheckbox?: (pageId: string) => void,
-  onClickItem?: (pageId: string) => void,
-  onClickDeleteButton?: (pageId: string) => void,
+  onClickCheckbox?: (pageId: string) => Promise<void>,
+  onClickItem?: (pageId: string) => Promise<void>,
+  onClickDeleteButton?: (pageId: string) => Promise<void>,
 }
 
 export const PageListItemL: FC<Props> = memo((props:Props) => {
@@ -132,6 +130,7 @@ export const PageListItemL: FC<Props> = memo((props:Props) => {
               <Clamp lines={2}>
                 {
                   elasticSearchResult != null && elasticSearchResult?.snippet.length !== 0 ? (
+                    // eslint-disable-next-line react/no-danger
                     <div dangerouslySetInnerHTML={{ __html: elasticSearchResult.snippet }}></div>
                   ) : (
                     <div>{ shortBody != null ? shortBody : 'Loading ...' }</div> // TODO: improve indicator

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

@@ -1,22 +1,19 @@
 import React, {
-  useCallback, useState, FC, useEffect, memo,
+  useCallback, useState, FC, useEffect,
 } from 'react';
 import nodePath from 'path';
 import { useTranslation } from 'react-i18next';
-import { pagePathUtils } from '@growi/core';
 import { useDrag, useDrop } from 'react-dnd';
 import { toastWarning } from '~/client/util/apiNotification';
 
 import { ItemNode } from './ItemNode';
-import { IPageHasId } from '~/interfaces/page';
 import { useSWRxPageChildren } from '../../../stores/page-listing';
 import ClosableTextInput, { AlertInfo, AlertType } from '../../Common/ClosableTextInput';
 import { AsyncPageItemControl } from '../../Common/Dropdown/PageItemControl';
 import { IPageForPageDeleteModal } from '~/components/PageDeleteModal';
 
 import TriangleIcon from '~/components/Icons/TriangleIcon';
-
-const { isTopPage, isUserNamePage } = pagePathUtils;
+import { bookmark, unbookmark } from '~/client/services/page-operation';
 
 
 interface ItemProps {
@@ -42,6 +39,12 @@ const markTarget = (children: ItemNode[], targetPathOrId?: string): void => {
 };
 
 
+const bookmarkMenuItemClickHandler = async(_pageId: string, _newValue: boolean): Promise<void> => {
+  const bookmarkOperation = _newValue ? bookmark : unbookmark;
+  await bookmarkOperation(_pageId);
+};
+
+
 type ItemCountProps = {
   descendantCount: number
 }
@@ -73,9 +76,6 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
 
   const hasDescendants = (page.descendantCount != null && page?.descendantCount > 0);
 
-  const isDeletable = true; // TODO: retrieve from IPageInfo
-  // const isDeletable = !page.isEmpty && !isTopPage(page.path as string) && !isUserNamePage(page.path as string);
-
   const [{ isDragging }, drag] = useDrag(() => ({
     type: 'PAGE_TREE',
     item: { page },
@@ -120,7 +120,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
     setNewPageInputShown(true);
   }, []);
 
-  const onClickDeleteButton = useCallback(() => {
+  const onClickDeleteButton = useCallback(async(_pageId: string): Promise<void> => {
     if (onClickDeleteByPage == null) {
       return;
     }
@@ -141,7 +141,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   }, [page, onClickDeleteByPage]);
 
 
-  const onClickRenameButton = useCallback(() => {
+  const onClickRenameButton = useCallback(async(_pageId: string): Promise<void> => {
     setRenameInputShown(true);
   }, []);
 
@@ -171,7 +171,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
   // didMount
   useEffect(() => {
     if (hasChildren()) setIsOpen(true);
-  }, []);
+  }, [hasChildren]);
 
   /*
    * Make sure itemNode.children and currentChildren are synced
@@ -181,7 +181,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       markTarget(children, targetPathOrId);
       setCurrentChildren(children);
     }
-  }, []);
+  }, [children, currentChildren.length, targetPathOrId]);
 
   /*
    * When swr fetch succeeded
@@ -192,7 +192,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
       markTarget(newChildren, targetPathOrId);
       setCurrentChildren(newChildren);
     }
-  }, [data, isOpen]);
+  }, [data, error, isOpen, targetPathOrId]);
 
   return (
     <div className={`grw-pagetree-item-container ${isOver ? 'grw-pagetree-is-over' : ''}`}>
@@ -239,6 +239,7 @@ const Item: FC<ItemProps> = (props: ItemProps) => {
           <AsyncPageItemControl
             pageId={page._id}
             isEnableActions={isEnableActions}
+            onClickBookmarkMenuItem={bookmarkMenuItemClickHandler}
             onClickDeleteMenuItem={onClickDeleteButton}
             onClickRenameMenuItem={onClickRenameButton}
           />