Parcourir la source

Merge pull request #4711 from weseek/feat/77524-81076-SubNavButton-final

feat/#81076 Bookmark&PageManagement
Yuki Takei il y a 4 ans
Parent
commit
90dfed7123

+ 12 - 23
packages/app/src/components/BookmarkButton.jsx

@@ -10,7 +10,7 @@ import { apiv3Put } from '~/client/util/apiv3-client';
 
 import AppContainer from '~/client/services/AppContainer';
 
-class BookmarkButton extends React.Component {
+class LegacyBookmarkButton extends React.Component {
 
   constructor(props) {
     super(props);
@@ -19,25 +19,11 @@ class BookmarkButton extends React.Component {
   }
 
   async handleClick() {
-    const {
-      appContainer, pageId, isBookmarked, onChangeInvoked,
-    } = this.props;
-    const { isGuestUser } = appContainer;
 
-    if (isGuestUser) {
+    if (this.props.onBookMarkClicked == null) {
       return;
     }
-
-    try {
-      const bool = !isBookmarked;
-      await apiv3Put('/bookmarks', { pageId, bool });
-      if (onChangeInvoked != null) {
-        onChangeInvoked();
-      }
-    }
-    catch (err) {
-      toastError(err);
-    }
+    this.props.onBookMarkClicked();
   }
 
   render() {
@@ -77,21 +63,24 @@ class BookmarkButton extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer]);
+const LegacyBookmarkButtonWrapper = withUnstatedContainers(LegacyBookmarkButton, [AppContainer]);
 
-BookmarkButton.propTypes = {
+LegacyBookmarkButton.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
-  pageId: PropTypes.string.isRequired,
   isBookmarked: PropTypes.bool.isRequired,
   sumOfBookmarks: PropTypes.number,
-  onChangeInvoked: PropTypes.func,
   t: PropTypes.func.isRequired,
   size: PropTypes.string,
+  onBookMarkClicked: PropTypes.func,
 };
 
-BookmarkButton.defaultProps = {
+LegacyBookmarkButton.defaultProps = {
   size: 'md',
 };
 
-export default withTranslation()(BookmarkButtonWrapper);
+const BookmarkButton = (props) => {
+  return <LegacyBookmarkButtonWrapper {...props}></LegacyBookmarkButtonWrapper>;
+};
+
+export default withTranslation()(BookmarkButton);

+ 3 - 11
packages/app/src/components/ComparePathsTable.jsx

@@ -3,17 +3,14 @@ import PropTypes from 'prop-types';
 
 import { withTranslation } from 'react-i18next';
 import { pagePathUtils } from '@growi/core';
-import { withUnstatedContainers } from './UnstatedUtils';
 
-import PageContainer from '~/client/services/PageContainer';
 
 const { convertToNewAffiliationPath } = pagePathUtils;
 
 function ComparePathsTable(props) {
   const {
-    subordinatedPages, pageContainer, newPagePath, t,
+    path, subordinatedPages, newPagePath, t,
   } = props;
-  const { path } = pageContainer.state;
 
   return (
     <table className="table table-bordered grw-compare-paths-table">
@@ -45,18 +42,13 @@ function ComparePathsTable(props) {
 }
 
 
-/**
- * Wrapper component for using unstated
- */
-const PageDuplicateModallWrapper = withUnstatedContainers(ComparePathsTable, [PageContainer]);
-
 ComparePathsTable.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
 
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  path: PropTypes.string.isRequired,
   subordinatedPages: PropTypes.array.isRequired,
   newPagePath: PropTypes.string.isRequired,
 };
 
 
-export default withTranslation()(PageDuplicateModallWrapper);
+export default withTranslation()(ComparePathsTable);

+ 23 - 3
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -25,14 +25,26 @@ const GrowiSubNavigation = (props) => {
   } = props;
   const { isDrawerMode, editorMode, isDeviceSmallerThanMd } = navigationContainer.state;
   const {
-    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isPageExist, tags,
+    pageId,
+    revisionId,
+    path,
+    isDeletable,
+    isAbleToDeleteCompletely,
+    createdAt,
+    creator,
+    updatedAt,
+    revisionAuthor,
+    isPageExist,
+    isTrashPage,
+    tags,
   } = pageContainer.state;
 
-  const { isGuestUser } = appContainer;
+  const { isGuestUser, isSharedUser } = appContainer;
   const isEditorMode = editorMode !== 'view';
   // Tags cannot be edited while the new page and editorMode is view
   const isTagLabelHidden = (editorMode !== 'edit' && !isPageExist);
 
+  const isAbleToShowPageManagement = isPageExist && !isTrashPage && !isSharedUser;
   function onPageEditorModeButtonClicked(viewType) {
     navigationContainer.setEditorMode(viewType);
   }
@@ -85,7 +97,15 @@ const GrowiSubNavigation = (props) => {
 
         <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
-            <SubNavButtons isCompactMode={isCompactMode} pageId={pageId} />
+            <SubNavButtons
+              isCompactMode={isCompactMode}
+              pageId={pageId}
+              revisionId={revisionId}
+              path={path}
+              isDeletable={isDeletable}
+              isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+              willShowPageManagement={isAbleToShowPageManagement}
+            />
           </div>
           <div className="mt-2">
             {pageContainer.isAbleToShowPageEditorModeManager && (

+ 44 - 12
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -8,6 +8,7 @@ import { withUnstatedContainers } from '../UnstatedUtils';
 import PageReactionButtons from '../PageReactionButtons';
 import PageManagement from '../Page/PageManagement';
 import { useSWRPageInfo } from '../../stores/page';
+import { useSWRBookmarkInfo } from '../../stores/bookmark';
 import { toastError } from '../../client/util/apiNotification';
 import { apiv3Put } from '../../client/util/apiv3-client';
 
@@ -17,18 +18,24 @@ type SubNavButtonsProps= {
   navigationContainer: NavigationContainer,
   isCompactMode?: boolean,
   pageId: string,
+  revisionId: string,
+  path: string,
+  willShowPageManagement: boolean,
+  isDeletable: boolean,
+  isAbleToDeleteCompletely: boolean,
 }
 const SubNavButtons: FC<SubNavButtonsProps> = (props: SubNavButtonsProps) => {
   const {
-    appContainer, navigationContainer, isCompactMode, pageId,
+    appContainer, navigationContainer, isCompactMode, pageId, revisionId, path, willShowPageManagement, isDeletable, isAbleToDeleteCompletely,
   } = props;
   const { editorMode } = navigationContainer.state;
   const isViewMode = editorMode === 'view';
+  const { isGuestUser } = appContainer;
+
   const { data: pageInfo, error: pageInfoError, mutate: mutatePageInfo } = useSWRPageInfo(pageId);
+  const { data: bookmarkInfo, error: bookmarkInfoError, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(pageId);
 
   const likeClickhandler = useCallback(async() => {
-    const { isGuestUser } = appContainer;
-
     if (isGuestUser) {
       return;
     }
@@ -43,11 +50,31 @@ const SubNavButtons: FC<SubNavButtonsProps> = (props: SubNavButtonsProps) => {
     }
   }, [pageInfo]);
 
+  const bookmarkClickHandler = useCallback(async() => {
+    if (isGuestUser) {
+      return;
+    }
+    try {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      await apiv3Put('/bookmarks', { pageId, bool: !bookmarkInfo!.isBookmarked });
+      mutateBookmarkInfo();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [bookmarkInfo]);
+
 
   if (pageInfoError != null || pageInfo == null) {
     return <></>;
   }
+
+  if (bookmarkInfoError != null || bookmarkInfo == null) {
+    return <></>;
+  }
+
   const { sumOfLikers, likerIds, isLiked } = pageInfo;
+  const { sumOfBookmarks, isBookmarked } = bookmarkInfo;
 
   return (
     <>
@@ -58,18 +85,23 @@ const SubNavButtons: FC<SubNavButtonsProps> = (props: SubNavButtonsProps) => {
           likerIds={likerIds}
           isLiked={isLiked}
           onLikeClicked={likeClickhandler}
+          sumOfBookmarks={sumOfBookmarks}
+          isBookmarked={isBookmarked}
+          onBookMarkClicked={bookmarkClickHandler}
         >
         </PageReactionButtons>
       )}
-      {/*
-        TODO :
-        once 80335 is done, merge 77543 branch(parent of 80335) into 77524.
-        (pageContainer dependencies in bookmark, delete modal, rename etc are removed)
-        then place PageManagement here.
-        TASK: https://estoc.weseek.co.jp/redmine/issues/81076
-        CONDITION :isAbleToShowPageManagement = !isNotFoundPage && !isTrashPage && !isSharedUser
-      */}
-      {/* if (CONDITION) then <PageManagement isCompactMode> */}
+      {willShowPageManagement && (
+        <PageManagement
+          pageId={pageId}
+          revisionId={revisionId}
+          path={path}
+          isCompactMode={isCompactMode}
+          isDeletable={isDeletable}
+          isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+        >
+        </PageManagement>
+      )}
     </>
   );
 };

+ 18 - 13
packages/app/src/components/Page/PageManagement.jsx

@@ -7,7 +7,6 @@ import urljoin from 'url-join';
 import { pagePathUtils } from '@growi/core';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 import PageDeleteModal from '../PageDeleteModal';
 import PageRenameModal from '../PageRenameModal';
 import PageDuplicateModal from '../PageDuplicateModal';
@@ -18,13 +17,10 @@ import PresentationIcon from '../Icons/PresentationIcon';
 const { isTopPage } = pagePathUtils;
 
 
-const PageManagement = (props) => {
+const LegacyPageManagemenet = (props) => {
   const {
-    t, appContainer, pageContainer, isCompactMode,
+    t, appContainer, isCompactMode, pageId, revisionId, path, isDeletable, isAbleToDeleteCompletely,
   } = props;
-  const {
-    pageId, revisionId, path, isDeletable, isAbleToDeleteCompletely,
-  } = pageContainer.state;
 
   const { currentUser } = appContainer;
   const isTopPagePath = isTopPage(path);
@@ -33,6 +29,7 @@ const PageManagement = (props) => {
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
   const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
   const [isPagePresentationModalShown, setIsPagePresentationModalShown] = useState(false);
+  const presentationHref = urljoin(window.location.origin, path, '?presentation=1');
 
   function openPageRenameModalHandler() {
     setIsPageRenameModalShown(true);
@@ -86,7 +83,6 @@ const PageManagement = (props) => {
   // }
 
   async function exportPageHandler(format) {
-    const { pageId, revisionId } = pageContainer.state;
     const url = new URL(urljoin(window.location.origin, '_api/v3/page/export', pageId));
     url.searchParams.append('format', format);
     url.searchParams.append('revisionId', revisionId);
@@ -193,7 +189,7 @@ const PageManagement = (props) => {
         <PagePresentationModal
           isOpen={isPagePresentationModalShown}
           onClose={closePagePresentationModalHandler}
-          href="?presentation=1"
+          href={presentationHref}
         />
       </>
     );
@@ -249,19 +245,28 @@ const PageManagement = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const PageManagementWrapper = withUnstatedContainers(PageManagement, [AppContainer, PageContainer]);
+const LegacyPageManagemenetWrapper = withUnstatedContainers(LegacyPageManagemenet, [AppContainer]);
 
 
-PageManagement.propTypes = {
+LegacyPageManagemenet.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
+  path: PropTypes.string.isRequired,
+  isDeletable: PropTypes.bool.isRequired,
+  isAbleToDeleteCompletely: PropTypes.bool.isRequired,
 
   isCompactMode: PropTypes.bool,
 };
 
-PageManagement.defaultProps = {
+LegacyPageManagemenet.defaultProps = {
   isCompactMode: false,
 };
 
-export default withTranslation()(PageManagementWrapper);
+const PageManagement = (props) => {
+  return <LegacyPageManagemenetWrapper {...props}></LegacyPageManagemenetWrapper>;
+};
+export default withTranslation()(PageManagement);

+ 8 - 2
packages/app/src/components/PageDeleteModal.jsx

@@ -91,6 +91,12 @@ const PageDeleteModal = (props) => {
     );
   }
 
+  // DeleteCompletely is currently disabled
+  // TODO1 : Retrive isAbleToDeleteCompleltly state everywhere in the system via swr.
+  // Story: https://redmine.weseek.co.jp/issues/82222
+
+  // TODO2 : use toaster
+  // TASK : https://redmine.weseek.co.jp/issues/82299
   function renderDeleteCompletelyForm() {
     return (
       <div className="custom-control custom-checkbox custom-checkbox-danger">
@@ -99,12 +105,12 @@ const PageDeleteModal = (props) => {
           name="completely"
           id="deleteCompletely"
           type="checkbox"
-          disabled={!isAbleToDeleteCompletely}
+          disabled
           checked={isDeleteCompletely}
           onChange={changeIsDeleteCompletelyHandler}
         />
         <label className="custom-control-label text-danger" htmlFor="deleteCompletely">
-          { t('modal_delete.delete_completely') }
+          { t('modal_delete.delete_completely')}
           <p className="form-text text-muted mt-0"> { t('modal_delete.completely') }</p>
         </label>
         {!isAbleToDeleteCompletely

+ 1 - 1
packages/app/src/components/PageDuplicateModal.jsx

@@ -188,7 +188,7 @@ const PageDuplicateModal = (props) => {
             )}
           </div>
           <div>
-            {isDuplicateRecursively && <ComparePathsTable subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
+            {isDuplicateRecursively && <ComparePathsTable path={path} subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
             {isDuplicateRecursively && existingPaths.length !== 0 && <DuplicatePathsTable existingPaths={existingPaths} oldPagePath={pageNameInput} />}
           </div>
         </div>

+ 7 - 10
packages/app/src/components/PageReactionButtons.tsx

@@ -1,5 +1,6 @@
 import React, { FC, useState, useEffect } from 'react';
 import LikeButtons from './LikeButtons';
+import BookmarkButton from './BookmarkButton';
 
 
 type Props = {
@@ -7,13 +8,16 @@ type Props = {
   sumOfLikers: number,
   likerIds: string[],
   isLiked: boolean,
-  onLikeClicked: (isLiked : boolean)=>void,
+  sumOfBookmarks: number,
+  isBookmarked: boolean,
+  onLikeClicked: ()=>void,
+  onBookMarkClicked: ()=>void,
 }
 
 
 const PageReactionButtons : FC<Props> = (props: Props) => {
   const {
-    pageId, sumOfLikers, likerIds, isLiked, onLikeClicked,
+    pageId, sumOfLikers, likerIds, isLiked, sumOfBookmarks, isBookmarked, onLikeClicked, onBookMarkClicked,
   } = props;
 
 
@@ -23,14 +27,7 @@ const PageReactionButtons : FC<Props> = (props: Props) => {
         <LikeButtons onLikeClicked={onLikeClicked} pageId={pageId} likerIds={likerIds} sumOfLikers={sumOfLikers} isLiked={isLiked}></LikeButtons>
       </span>
       <span>
-        {/*
-          TODO:
-          once 80335 is done, merge 77543 branch(parent of 80335) into 77524.
-          (pageContainer dependencies in bookmark, delete modal, rename etc are removed)
-          then place BookMarkButton here
-          TASK: https://estoc.weseek.co.jp/redmine/issues/81076
-        */}
-        {/* <BookmarkButton></BookmarkButton> */}
+        <BookmarkButton sumOfBookmarks={sumOfBookmarks} isBookmarked={isBookmarked} onBookMarkClicked={onBookMarkClicked}></BookmarkButton>
       </span>
     </>
   );

+ 1 - 1
packages/app/src/components/PageRenameModal.jsx

@@ -195,7 +195,7 @@ const PageRenameModal = (props) => {
               </label>
             </div>
           )}
-          {isRenameRecursively && <ComparePathsTable subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
+          {isRenameRecursively && <ComparePathsTable path={path} subordinatedPages={subordinatedPages} newPagePath={pageNameInput} />}
           {isRenameRecursively && existingPaths.length !== 0 && <DuplicatedPathsTable existingPaths={existingPaths} oldPagePath={pageNameInput} />}
         </div>
 

+ 19 - 23
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -17,30 +17,26 @@ type Props ={
 
 
 const SearchResultContent: FC<Props> = (props: Props) => {
-  // Temporaly workaround for lint error
-  // later needs to be fixed: RevisoinRender to typescriptcomponet
-  const RevisionRenderTypeAny: any = RevisionLoader;
-  const renderPage = (searchResultData) => {
-    const page = searchResultData?.pageData || {};
-    const growiRenderer = props.appContainer.getRenderer('searchresult');
-    let showTags = false;
-    if (page.tags != null && page.tags.length > 0) { showTags = true }
-    return (
-      <div key={page._id} className="search-result-page mb-5">
-        <SearchResultContentSubNavigation pageId={page._id} path={page.path} />
-        <RevisionRenderTypeAny
-          growiRenderer={growiRenderer}
-          pageId={page._id}
-          pagePath={page.path}
-          revisionId={page.revision}
-          highlightKeywords={props.searchingKeyword}
-        />
-      </div>
-    );
-  };
-  const content = renderPage(props.focusedSearchResultData);
+  const page = props.focusedSearchResultData?.pageData;
+  // return if page is null
+  if (page == null) return <></>;
+  const growiRenderer = props.appContainer.getRenderer('searchresult');
   return (
-    <div>{content}</div>
+    <div key={page._id} className="search-result-page mb-5">
+      <SearchResultContentSubNavigation
+        pageId={page._id}
+        revisionId={page.revision}
+        path={page.path}
+      >
+      </SearchResultContentSubNavigation>
+      <RevisionLoader
+        growiRenderer={growiRenderer}
+        pageId={page._id}
+        pagePath={page.path}
+        revisionId={page.revision}
+        highlightKeywords={props.searchingKeyword}
+      />
+    </div>
   );
 };
 

+ 22 - 2
packages/app/src/components/SearchPage/SearchResultContentSubNavigation.tsx

@@ -1,4 +1,5 @@
 import React, { FC, useCallback } from 'react';
+import { pagePathUtils } from '@growi/core';
 import PagePathNav from '../PagePathNav';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../client/services/AppContainer';
@@ -11,6 +12,7 @@ import SubNavButtons from '../Navbar/SubNavButtons';
 type Props = {
   appContainer:AppContainer
   pageId: string,
+  revisionId: string,
   path: string,
   isSignleLineMode?: boolean,
   isCompactMode?: boolean,
@@ -19,9 +21,11 @@ type Props = {
 
 const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
   const {
-    appContainer, pageId, path, isCompactMode, isSignleLineMode,
+    appContainer, pageId, revisionId, path, isCompactMode, isSignleLineMode,
   } = props;
 
+  const { isTrashPage, isDeletablePage } = pagePathUtils;
+
   const { data: tagInfoData, error: tagInfoError, mutate: mutateTagInfo } = useSWRTagsInfo(pageId);
 
   const tagsUpdatedHandler = useCallback(async(newTags) => {
@@ -38,7 +42,9 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
   if (tagInfoError != null || tagInfoData == null) {
     return <></>;
   }
+  const isPageDeletable = isDeletablePage(path);
   const { isSharedUser } = appContainer;
+  const isAbleToShowPageManagement = !(isTrashPage(path)) && !isSharedUser;
   return (
     <div className={`grw-subnav container-fluid d-flex align-items-center justify-content-between ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}>
       {/* Left side */}
@@ -51,8 +57,22 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
         <PagePathNav pageId={pageId} pagePath={path} isCompactMode={isCompactMode} isSingleLineMode={isSignleLineMode} />
       </div>
       {/* Right side */}
+      {/*
+        DeleteCompletely is currently disabled
+        TODO : Retrive isAbleToDeleteCompleltly state everywhere in the system via swr.
+        story: https://redmine.weseek.co.jp/issues/82222
+      */}
       <div className="d-flex">
-        <SubNavButtons isCompactMode={isCompactMode} pageId={pageId}></SubNavButtons>
+        <SubNavButtons
+          isCompactMode={isCompactMode}
+          pageId={pageId}
+          revisionId={revisionId}
+          path={path}
+          isDeletable={isPageDeletable}
+          // isAbleToDeleteCompletely={}
+          willShowPageManagement={isAbleToShowPageManagement}
+        >
+        </SubNavButtons>
       </div>
     </div>
   );

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

@@ -0,0 +1,4 @@
+export type IBookmarkInfo = {
+  sumOfBookmarks: number;
+  isBookmarked: boolean,
+};

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

@@ -0,0 +1,16 @@
+import useSWR, { SWRResponse } from 'swr';
+import { apiv3Get } from '../client/util/apiv3-client';
+import { IBookmarkInfo } from '../interfaces/bookmark-info';
+
+
+export const useSWRBookmarkInfo = (pageId: string): SWRResponse<IBookmarkInfo, Error> => {
+  return useSWR(
+    `/bookmarks/info?pageId=${pageId}`,
+    endpoint => apiv3Get(endpoint).then((response) => {
+      return {
+        sumOfBookmarks: response.data.sumOfBookmarks,
+        isBookmarked: response.data.isBookmarked,
+      };
+    }),
+  );
+};