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

Merge branch 'feat/77524-search-result-conent-page' into feat/77524-81110-show-liker-icon

# Conflicts:
#	packages/app/src/components/Navbar/SubNavButtons.tsx
#	packages/app/src/components/PageReactionButtons.tsx
Yohei-Shiina 4 лет назад
Родитель
Сommit
b2233a423c

+ 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);

+ 54 - 7
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -1,10 +1,11 @@
-import React from 'react';
+import React, { useCallback } from 'react';
 import PropTypes from 'prop-types';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
+import EditorContainer from '~/client/services/EditorContainer';
 
 import TagLabels from '../Page/TagLabels';
 import SubNavButtons from './SubNavButtons';
@@ -15,24 +16,61 @@ import DrawerToggler from './DrawerToggler';
 
 import PagePathNav from '../PagePathNav';
 
+import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiPost } from '~/client/util/apiv1-client';
+
 const GrowiSubNavigation = (props) => {
   const {
-    appContainer, navigationContainer, pageContainer, isCompactMode,
+    appContainer, navigationContainer, pageContainer, editorContainer, isCompactMode,
   } = props;
   const { isDrawerMode, editorMode, isDeviceSmallerThanMd } = navigationContainer.state;
   const {
-    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isPageExist,
+    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);
   }
 
+  const tagsUpdatedHandler = useCallback(async(newTags) => {
+    // It will not be reflected in the DB until the page is refreshed
+    if (editorMode === 'edit') {
+      return editorContainer.setState({ tags: newTags });
+    }
+
+    try {
+      const { tags } = await apiPost('/tags.update', { pageId, tags: newTags });
+
+      // update pageContainer.state
+      pageContainer.setState({ tags });
+      // update editorContainer.state
+      editorContainer.setState({ tags });
+
+      toastSuccess('updated tags successfully');
+    }
+    catch (err) {
+      toastError(err, 'fail to update tags');
+    }
+  // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [pageId]);
+
   return (
     <div className={`grw-subnav container-fluid d-flex align-items-center justify-content-between ${isCompactMode ? 'grw-subnav-compact d-print-none' : ''}`}>
 
@@ -47,7 +85,7 @@ const GrowiSubNavigation = (props) => {
         <div className="grw-path-nav-container">
           { pageContainer.isAbleToShowTagLabel && !isCompactMode && !isTagLabelHidden && (
             <div className="grw-taglabels-container">
-              <TagLabels editorMode={editorMode} />
+              <TagLabels tags={tags} tagsUpdateInvoked={tagsUpdatedHandler} />
             </div>
           ) }
           <PagePathNav pageId={pageId} pagePath={path} isSingleLineMode={isEditorMode} isCompactMode={isCompactMode} />
@@ -59,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 && (
@@ -92,13 +138,14 @@ const GrowiSubNavigation = (props) => {
 /**
  * Wrapper component for using unstated
  */
-const GrowiSubNavigationWrapper = withUnstatedContainers(GrowiSubNavigation, [AppContainer, NavigationContainer, PageContainer]);
+const GrowiSubNavigationWrapper = withUnstatedContainers(GrowiSubNavigation, [AppContainer, NavigationContainer, PageContainer, EditorContainer]);
 
 
 GrowiSubNavigation.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 
   isCompactMode: PropTypes.bool,
 };

+ 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';
 import { useSWRxLikerList } from '../../stores/user';
@@ -17,19 +18,26 @@ 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: likers } = useSWRxLikerList(pageInfo?.likerIds);
+  const { data: bookmarkInfo, error: bookmarkInfoError, mutate: mutateBookmarkInfo } = useSWRBookmarkInfo(pageId);
 
   const likeClickhandler = useCallback(async() => {
     const { isGuestUser } = appContainer;
-
     if (isGuestUser) {
       return;
     }
@@ -44,32 +52,56 @@ 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, isLiked } = pageInfo;
+  const { sumOfBookmarks, isBookmarked } = bookmarkInfo;
 
   return (
     <>
       {isViewMode && (
         <PageReactionButtons
-          pageId={pageId}
           sumOfLikers={sumOfLikers}
           isLiked={isLiked}
           likers={likers || []}
           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);

+ 9 - 48
packages/app/src/components/Page/TagLabels.jsx

@@ -2,12 +2,9 @@ import React, { Suspense } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
-import EditorContainer from '~/client/services/EditorContainer';
 
 import RenderTagLabels from './RenderTagLabels';
 import TagEditModal from './TagEditModal';
@@ -23,18 +20,8 @@ class TagLabels extends React.Component {
 
     this.openEditorModal = this.openEditorModal.bind(this);
     this.closeEditorModal = this.closeEditorModal.bind(this);
-    this.tagsUpdatedHandler = this.tagsUpdatedHandler.bind(this);
   }
 
-  /**
-   * @return tags data
-   *   1. pageContainer.state.tags if editorMode is view
-   *   2. editorContainer.state.tags if editorMode is edit
-   */
-  getTagData() {
-    const { editorContainer, pageContainer, editorMode } = this.props;
-    return (editorMode === 'edit') ? editorContainer.state.tags : pageContainer.state.tags;
-  }
 
   openEditorModal() {
     this.setState({ isTagEditModalShown: true });
@@ -44,37 +31,9 @@ class TagLabels extends React.Component {
     this.setState({ isTagEditModalShown: false });
   }
 
-  async tagsUpdatedHandler(newTags) {
-    const {
-      appContainer, editorContainer, pageContainer, editorMode,
-    } = this.props;
-
-    const { pageId } = pageContainer.state;
-
-    // It will not be reflected in the DB until the page is refreshed
-    if (editorMode === 'edit') {
-      return editorContainer.setState({ tags: newTags });
-    }
-
-    try {
-      const { tags } = await appContainer.apiPost('/tags.update', { pageId, tags: newTags });
-
-      // update pageContainer.state
-      pageContainer.setState({ tags });
-      // update editorContainer.state
-      editorContainer.setState({ tags });
-
-      toastSuccess('updated tags successfully');
-    }
-    catch (err) {
-      toastError(err, 'fail to update tags');
-    }
-  }
-
 
   render() {
-    const tags = this.getTagData();
-    const { appContainer } = this.props;
+    const { appContainer, tagsUpdateInvoked, tags } = this.props;
 
     return (
       <>
@@ -95,7 +54,7 @@ class TagLabels extends React.Component {
           isOpen={this.state.isTagEditModalShown}
           onClose={this.closeEditorModal}
           appContainer={this.props.appContainer}
-          onTagsUpdated={this.tagsUpdatedHandler}
+          onTagsUpdated={tagsUpdateInvoked}
         />
 
       </>
@@ -107,16 +66,18 @@ class TagLabels extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const TagLabelsWrapper = withUnstatedContainers(TagLabels, [AppContainer, PageContainer, EditorContainer]);
+const TagLabelsUnstatedWrapper = withUnstatedContainers(TagLabels, [AppContainer]);
 
 TagLabels.propTypes = {
   t: PropTypes.func.isRequired, // i18next
 
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-
-  editorMode: PropTypes.string.isRequired,
+  tags: PropTypes.arrayOf(PropTypes.object).isRequired,
+  tagsUpdateInvoked: PropTypes.func,
 };
 
+// wrapping tsx component returned by withUnstatedContainers to avoid type error when this component used in other tsx components.
+const TagLabelsWrapper = (props) => {
+  return <TagLabelsUnstatedWrapper {...props}></TagLabelsUnstatedWrapper>;
+};
 export default withTranslation()(TagLabelsWrapper);

+ 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>

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

@@ -1,19 +1,22 @@
 import React, { FC } from 'react';
 import LikeButtons from './LikeButtons';
 import { IUser } from '../interfaces/user';
+import BookmarkButton from './BookmarkButton';
 
 type Props = {
-  pageId: string,
   sumOfLikers: number,
   isLiked: boolean,
   likers: IUser[],
   onLikeClicked?: ()=>void,
+  sumOfBookmarks: number,
+  isBookmarked: boolean,
+  onBookMarkClicked: ()=>void,
 }
 
 
 const PageReactionButtons : FC<Props> = (props: Props) => {
   const {
-    sumOfLikers, isLiked, likers, onLikeClicked,
+    sumOfLikers, isLiked, likers, onLikeClicked, sumOfBookmarks, isBookmarked, onBookMarkClicked,
   } = props;
 
 
@@ -29,14 +32,7 @@ const PageReactionButtons : FC<Props> = (props: Props) => {
         </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>
   );
 };
 

+ 47 - 11
packages/app/src/components/SearchPage/SearchResultContentSubNavigation.tsx

@@ -1,12 +1,18 @@
-import React, { FC } from 'react';
+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';
+import TagLabels from '../Page/TagLabels';
+import { toastSuccess, toastError } from '../../client/util/apiNotification';
+import { apiPost } from '../../client/util/apiv1-client';
+import { useSWRTagsInfo } from '../../stores/page';
 import SubNavButtons from '../Navbar/SubNavButtons';
 
 type Props = {
   appContainer:AppContainer
   pageId: string,
+  revisionId: string,
   path: string,
   isSignleLineMode?: boolean,
   isCompactMode?: boolean,
@@ -15,28 +21,58 @@ 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) => {
+    try {
+      await apiPost('/tags.update', { pageId, tags: newTags });
+      toastSuccess('updated tags successfully');
+      mutateTagInfo();
+    }
+    catch (err) {
+      toastError(err, 'fail to update tags');
+    }
+  }, [pageId, mutateTagInfo]);
+
+  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 */}
       <div className="grw-path-nav-container">
-        {/* TODO : refactor TagLabels in a way that it can be used independently from pageContainenr
-              TASK: #80623 https://estoc.weseek.co.jp/redmine/issues/80623
-              CONDITION reference : https://dev.growi.org/5fabddf8bbeb1a0048bcb9e9
-              userPage is not included in search so chekcing only isSharedUser or not.
-          */}
-        {/* { !isSharedUser &&  !isCompactMode &&  (
+        {!isSharedUser && !isCompactMode && (
           <div className="grw-taglabels-container">
-            <TagLabels editorMode={editorMode} />
+            <TagLabels tags={tagInfoData.tags} tagsUpdateInvoked={tagsUpdatedHandler} />
           </div>
-        )} */}
+        )}
         <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,
+};

+ 3 - 0
packages/app/src/interfaces/pageTagsInfo.ts

@@ -0,0 +1,3 @@
+export type IPageTagsInfo = {
+  tags : string[],
+}

+ 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,
+      };
+    }),
+  );
+};

+ 10 - 1
packages/app/src/stores/page.tsx

@@ -1,9 +1,11 @@
 import useSWR, { SWRResponse } from 'swr';
 
 import { apiv3Get } from '../client/util/apiv3-client';
+import { apiGet } from '../client/util/apiv1-client';
 
 import { IPage } from '../interfaces/page';
 import { IPagingResult } from '../interfaces/paging-result';
+import { IPageTagsInfo } from '../interfaces/pageTagsInfo';
 import { IPageInfo } from '../interfaces/page-info';
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -32,7 +34,6 @@ export const useSWRxPageList = (
   );
 };
 
-
 export const useSWRPageInfo = (pageId: string): SWRResponse<IPageInfo, Error> => {
   return useSWR(`/page/info?pageId=${pageId}`, endpoint => apiv3Get(endpoint).then((response) => {
     return {
@@ -45,3 +46,11 @@ export const useSWRPageInfo = (pageId: string): SWRResponse<IPageInfo, Error> =>
     };
   }));
 };
+
+export const useSWRTagsInfo = (pageId: string): SWRResponse<IPageTagsInfo, Error> => {
+  return useSWR(`/pages.getPageTag?pageId=${pageId}`, endpoint => apiGet(endpoint).then((response: IPageTagsInfo) => {
+    return {
+      tags: response.tags,
+    };
+  }));
+};