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

Merge branch 'feat/77524-search-result-conent-page' into feat/77524-80623-tagLable-refactoring

* feat/77524-search-result-conent-page: (53 commits)
  added comment
  80481 tsx clean code
  80481
  80481 fb
  80623 removed unnecessary import
  80481 SubNavButton swr
  80481 fb
  80481 swr
  80481 fix temporary workaround
  80481 wip use swr
  80481 fb
  80481 fb
  80481 fb
  80481 fb
  80481 ci
  80581 fb
  80481 fb
  80481 fb pageReactionButton
  80481 PageReactionButtons component
  adjusted code after master merged
  ...
Mao 4 лет назад
Родитель
Сommit
baf24f8fd9

+ 1 - 0
packages/app/package.json

@@ -133,6 +133,7 @@
     "prom-client": "^13.0.0",
     "prom-client": "^13.0.0",
     "react-card-flip": "^1.0.10",
     "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
     "react-image-crop": "^8.3.0",
+    "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "redis": "^3.0.2",
     "rimraf": "^3.0.0",
     "rimraf": "^3.0.0",

+ 0 - 16
packages/app/src/client/services/PageContainer.js

@@ -292,22 +292,6 @@ export default class PageContainer extends Container {
     await this.retrieveLikersAndSeenUsers();
     await this.retrieveLikersAndSeenUsers();
   }
   }
 
 
-  async toggleLike() {
-    {
-      const toggledIsLiked = !this.state.isLiked;
-      await this.appContainer.apiv3Put('/page/likes', { pageId: this.state.pageId, bool: toggledIsLiked });
-
-      await this.setState(state => ({
-        isLiked: toggledIsLiked,
-        sumOfLikers: toggledIsLiked ? state.sumOfLikers + 1 : state.sumOfLikers - 1,
-        likerIds: toggledIsLiked
-          ? [...this.state.likerIds, this.appContainer.currentUserId]
-          : state.likerIds.filter(id => id !== this.appContainer.currentUserId),
-      }));
-    }
-
-    await this.retrieveLikersAndSeenUsers();
-  }
 
 
   async retrieveLikersAndSeenUsers() {
   async retrieveLikersAndSeenUsers() {
     const { users } = await this.appContainer.apiGet('/users.list', { user_ids: [...this.state.likerIds, ...this.state.seenUserIds].join(',') });
     const { users } = await this.appContainer.apiGet('/users.list', { user_ids: [...this.state.likerIds, ...this.state.seenUserIds].join(',') });

+ 3 - 13
packages/app/src/components/CreateTemplateModal.jsx

@@ -6,14 +6,11 @@ import { Modal, ModalHeader, ModalBody } from 'reactstrap';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 import { pathUtils } from '@growi/core';
 import { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
 import urljoin from 'url-join';
-import { withUnstatedContainers } from './UnstatedUtils';
 
 
-import PageContainer from '~/client/services/PageContainer';
 
 
 const CreateTemplateModal = (props) => {
 const CreateTemplateModal = (props) => {
-  const { t, pageContainer } = props;
+  const { t, path } = props;
 
 
-  const { path } = pageContainer.state;
   const parentPath = pathUtils.addTrailingSlash(path);
   const parentPath = pathUtils.addTrailingSlash(path);
 
 
   function generateUrl(label) {
   function generateUrl(label) {
@@ -67,18 +64,11 @@ const CreateTemplateModal = (props) => {
 };
 };
 
 
 
 
-/**
- * Wrapper component for using unstated
- */
-const CreateTemplateModalWrapper = withUnstatedContainers(CreateTemplateModal, [PageContainer]);
-
-
 CreateTemplateModal.propTypes = {
 CreateTemplateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
+  path: PropTypes.string.isRequired,
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
 };
 };
 
 
-export default withTranslation()(CreateTemplateModalWrapper);
+export default withTranslation()(CreateTemplateModal);

+ 22 - 25
packages/app/src/components/LikeButtons.jsx

@@ -6,11 +6,11 @@ import { withTranslation } from 'react-i18next';
 import UserPictureList from './User/UserPictureList';
 import UserPictureList from './User/UserPictureList';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
-import { toastError } from '~/client/util/apiNotification';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 
 
-class LikeButtons extends React.Component {
+// TODO : user image not displayed in search page. Fix it.
+// task : https://estoc.weseek.co.jp/redmine/issues/81110
+class LegacyLikeButtons extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
@@ -30,28 +30,19 @@ class LikeButtons extends React.Component {
     }));
     }));
   }
   }
 
 
-  async handleClick() {
-    const { appContainer, pageContainer } = this.props;
-    const { isGuestUser } = appContainer;
 
 
-    if (isGuestUser) {
+  handleClick() {
+    if (this.props.onLikeClicked == null) {
       return;
       return;
     }
     }
-
-    try {
-      pageContainer.toggleLike();
-    }
-    catch (err) {
-      toastError(err);
-    }
+    this.props.onLikeClicked();
   }
   }
 
 
   render() {
   render() {
-    const { appContainer, pageContainer, t } = this.props;
-    const { isGuestUser } = appContainer;
     const {
     const {
-      state: { likers, sumOfLikers, isLiked },
-    } = pageContainer;
+      appContainer, likerIds, sumOfLikers, isLiked, t,
+    } = this.props;
+    const { isGuestUser } = appContainer;
 
 
     return (
     return (
       <div className="btn-group" role="group" aria-label="Like buttons">
       <div className="btn-group" role="group" aria-label="Like buttons">
@@ -76,7 +67,7 @@ class LikeButtons extends React.Component {
         <Popover placement="bottom" isOpen={this.state.isPopoverOpen} target="po-total-likes" toggle={this.togglePopover} trigger="legacy">
         <Popover placement="bottom" isOpen={this.state.isPopoverOpen} target="po-total-likes" toggle={this.togglePopover} trigger="legacy">
           <PopoverBody className="seen-user-popover">
           <PopoverBody className="seen-user-popover">
             <div className="px-2 text-right user-list-content text-truncate text-muted">
             <div className="px-2 text-right user-list-content text-truncate text-muted">
-              {likers.length ? <UserPictureList users={likers} /> : t('No users have liked this yet.')}
+              {likerIds.length ? <UserPictureList users={likerIds} /> : t('No users have liked this yet.')}
             </div>
             </div>
           </PopoverBody>
           </PopoverBody>
         </Popover>
         </Popover>
@@ -89,14 +80,20 @@ class LikeButtons extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const LikeButtonsWrapper = withUnstatedContainers(LikeButtons, [AppContainer, PageContainer]);
+const LegacyLikeButtonsWrapper = withUnstatedContainers(LegacyLikeButtons, [AppContainer]);
 
 
-LikeButtons.propTypes = {
+LegacyLikeButtons.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
+  onChangeInvoked: PropTypes.func,
+  pageId: PropTypes.string.isRequired,
+  likerIds: PropTypes.arrayOf(PropTypes.object).isRequired,
+  sumOfLikers: PropTypes.number.isRequired,
+  isLiked: PropTypes.bool.isRequired,
+  onLikeClicked: PropTypes.func,
   t: PropTypes.func.isRequired,
   t: PropTypes.func.isRequired,
-  size: PropTypes.string,
+};
+const LikeButtons = (props) => {
+  return <LegacyLikeButtonsWrapper {...props}></LegacyLikeButtonsWrapper>;
 };
 };
 
 
-export default withTranslation()(LikeButtonsWrapper);
+export default withTranslation()(LikeButtons);

+ 2 - 2
packages/app/src/components/Navbar/GrowiSubNavigation.jsx

@@ -8,7 +8,7 @@ import PageContainer from '~/client/services/PageContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 import EditorContainer from '~/client/services/EditorContainer';
 
 
 import TagLabels from '../Page/TagLabels';
 import TagLabels from '../Page/TagLabels';
-import SubnavButtons from './SubNavButtons';
+import SubNavButtons from './SubNavButtons';
 import PageEditorModeManager from './PageEditorModeManager';
 import PageEditorModeManager from './PageEditorModeManager';
 
 
 import AuthorInfo from './AuthorInfo';
 import AuthorInfo from './AuthorInfo';
@@ -84,7 +84,7 @@ const GrowiSubNavigation = (props) => {
 
 
         <div className="d-flex flex-column align-items-end">
         <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
           <div className="d-flex">
-            <SubnavButtons isCompactMode={isCompactMode} />
+            <SubNavButtons isCompactMode={isCompactMode} pageId={pageId} />
           </div>
           </div>
           <div className="mt-2">
           <div className="mt-2">
             {pageContainer.isAbleToShowPageEditorModeManager && (
             {pageContainer.isAbleToShowPageEditorModeManager && (

+ 0 - 80
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -1,80 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import AppContainer from '~/client/services/AppContainer';
-import NavigationContainer from '~/client/services/NavigationContainer';
-import PageContainer from '~/client/services/PageContainer';
-import { withUnstatedContainers } from '../UnstatedUtils';
-import loggerFactory from '~/utils/logger';
-
-import BookmarkButton from '../BookmarkButton';
-import LikeButtons from '../LikeButtons';
-import PageManagement from '../Page/PageManagement';
-
-const logger = loggerFactory('growi:SubnavButtons');
-
-const SubnavButtons = (props) => {
-  const {
-    appContainer, navigationContainer, pageContainer, isCompactMode,
-  } = props;
-
-  /* eslint-enable react/prop-types */
-
-  /* eslint-disable react/prop-types */
-  const PageReactionButtons = ({ pageContainer }) => {
-    const { pageId, isBookmarked, sumOfBookmarks } = pageContainer.state;
-
-    const onChangeInvoked = () => {
-      if (pageContainer.retrieveBookmarkInfo == null) { logger.error('retrieveBookmarkInfo is null') }
-      else { pageContainer.retrieveBookmarkInfo() }
-    };
-
-    return (
-      <>
-        {pageContainer.isAbleToShowLikeButtons && (
-          <span>
-            <LikeButtons />
-          </span>
-        )}
-        <span>
-          <BookmarkButton
-            pageId={pageId}
-            isBookmarked={isBookmarked}
-            sumOfBookmarks={sumOfBookmarks}
-            onChangeInvoked={onChangeInvoked}
-          />
-        </span>
-      </>
-    );
-  };
-  /* eslint-enable react/prop-types */
-
-  const { editorMode } = navigationContainer.state;
-  const isViewMode = editorMode === 'view';
-
-  return (
-    <>
-      {isViewMode && (
-        <>
-          {pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} />}
-          {pageContainer.isAbleToShowPageManagement && <PageManagement isCompactMode={isCompactMode} />}
-        </>
-      )}
-    </>
-  );
-};
-
-/**
- * Wrapper component for using unstated
- */
-const SubnavButtonsWrapper = withUnstatedContainers(SubnavButtons, [AppContainer, NavigationContainer, PageContainer]);
-
-
-SubnavButtons.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
-  isCompactMode: PropTypes.bool,
-};
-
-export default SubnavButtonsWrapper;

+ 87 - 0
packages/app/src/components/Navbar/SubNavButtons.tsx

@@ -0,0 +1,87 @@
+import React, {
+  FC, useCallback,
+} from 'react';
+import AppContainer from '../../client/services/AppContainer';
+import NavigationContainer from '../../client/services/NavigationContainer';
+import { withUnstatedContainers } from '../UnstatedUtils';
+
+import PageReactionButtons from '../PageReactionButtons';
+import PageManagement from '../Page/PageManagement';
+import { useSWRPageInfo } from '../../stores/page';
+import { toastError } from '../../client/util/apiNotification';
+import { apiv3Put } from '../../client/util/apiv3-client';
+
+
+type SubNavButtonsProps= {
+  appContainer: AppContainer,
+  navigationContainer: NavigationContainer,
+  isCompactMode?: boolean,
+  pageId: string,
+}
+const SubNavButtons: FC<SubNavButtonsProps> = (props: SubNavButtonsProps) => {
+  const {
+    appContainer, navigationContainer, isCompactMode, pageId,
+  } = props;
+  const { editorMode } = navigationContainer.state;
+  const isViewMode = editorMode === 'view';
+  const { data: pageInfo, error: pageInfoError, mutate: mutatePageInfo } = useSWRPageInfo(pageId);
+
+  const likeClickhandler = useCallback(async() => {
+    const { isGuestUser } = appContainer;
+
+    if (isGuestUser) {
+      return;
+    }
+
+    try {
+      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+      await apiv3Put('/page/likes', { pageId, bool: !pageInfo!.isLiked });
+      mutatePageInfo();
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }, [pageInfo]);
+
+
+  if (pageInfoError != null || pageInfo == null) {
+    return <></>;
+  }
+  const { sumOfLikers, likerIds, isLiked } = pageInfo;
+
+  return (
+    <>
+      {isViewMode && (
+        <PageReactionButtons
+          pageId={pageId}
+          sumOfLikers={sumOfLikers}
+          likerIds={likerIds}
+          isLiked={isLiked}
+          onLikeClicked={likeClickhandler}
+        >
+        </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> */}
+    </>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const SubNavButtonsUnstatedWrapper = withUnstatedContainers(SubNavButtons, [AppContainer, NavigationContainer]);
+
+// wrapping tsx component returned by withUnstatedContainers to avoid type error when this component used in other tsx components.
+const SubNavButtonsWrapper = (props) => {
+  return <SubNavButtonsUnstatedWrapper {...props}></SubNavButtonsUnstatedWrapper>;
+};
+
+export default SubNavButtonsWrapper;

+ 1 - 0
packages/app/src/components/Page/PageManagement.jsx

@@ -178,6 +178,7 @@ const PageManagement = (props) => {
           path={path}
           path={path}
         />
         />
         <CreateTemplateModal
         <CreateTemplateModal
+          path={path}
           isOpen={isPageTemplateModalShown}
           isOpen={isPageTemplateModalShown}
           onClose={closePageTemplateModalHandler}
           onClose={closePageTemplateModalHandler}
         />
         />

+ 7 - 4
packages/app/src/components/Page/RevisionLoader.jsx

@@ -13,7 +13,7 @@ import RevisionRenderer from './RevisionRenderer';
 /**
 /**
  * Load data from server and render RevisionBody component
  * Load data from server and render RevisionBody component
  */
  */
-class RevisionLoader extends React.Component {
+class LegacyRevisionLoader extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
@@ -116,9 +116,9 @@ class RevisionLoader extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const RevisionLoaderWrapper = withUnstatedContainers(RevisionLoader, [AppContainer]);
+const LegacyRevisionLoaderWrapper = withUnstatedContainers(LegacyRevisionLoader, [AppContainer]);
 
 
-RevisionLoader.propTypes = {
+LegacyRevisionLoader.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
@@ -129,4 +129,7 @@ RevisionLoader.propTypes = {
   highlightKeywords: PropTypes.string,
   highlightKeywords: PropTypes.string,
 };
 };
 
 
-export default RevisionLoaderWrapper;
+const RevisionLoader = (props) => {
+  return <LegacyRevisionLoaderWrapper {...props}></LegacyRevisionLoaderWrapper>;
+};
+export default RevisionLoader;

+ 39 - 0
packages/app/src/components/PageReactionButtons.tsx

@@ -0,0 +1,39 @@
+import React, { FC, useState, useEffect } from 'react';
+import LikeButtons from './LikeButtons';
+
+
+type Props = {
+  pageId: string,
+  sumOfLikers: number,
+  likerIds: string[],
+  isLiked: boolean,
+  onLikeClicked: (isLiked : boolean)=>void,
+}
+
+
+const PageReactionButtons : FC<Props> = (props: Props) => {
+  const {
+    pageId, sumOfLikers, likerIds, isLiked, onLikeClicked,
+  } = props;
+
+
+  return (
+    <>
+      <span>
+        <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> */}
+      </span>
+    </>
+  );
+};
+
+export default PageReactionButtons;

+ 3 - 3
packages/app/src/components/SearchPage.jsx

@@ -30,11 +30,11 @@ class SearchPage extends React.Component {
       searchedKeyword: '',
       searchedKeyword: '',
       searchedPages: [],
       searchedPages: [],
       searchResultMeta: {},
       searchResultMeta: {},
-      focusedPage: {},
+      focusedPage: null,
       selectedPages: new Set(),
       selectedPages: new Set(),
       searchResultCount: 0,
       searchResultCount: 0,
       activePage: 1,
       activePage: 1,
-      pagingLimit: 3, // change to an appropriate limit number
+      pagingLimit: 10, // change to an appropriate limit number
       excludeUsersHome: true,
       excludeUsersHome: true,
       excludeTrash: true,
       excludeTrash: true,
     };
     };
@@ -163,7 +163,7 @@ class SearchPage extends React.Component {
           searchedPages: [],
           searchedPages: [],
           searchResultMeta: {},
           searchResultMeta: {},
           searchResultCount: 0,
           searchResultCount: 0,
-          focusedPage: {},
+          focusedPage: null,
           activePage: 1,
           activePage: 1,
         });
         });
       }
       }

+ 2 - 2
packages/app/src/components/SearchPage/SearchPageLayout.tsx

@@ -19,7 +19,7 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
   return (
   return (
     <div className="content-main">
     <div className="content-main">
       <div className="search-result row" id="search-result">
       <div className="search-result row" id="search-result">
-        <div className="col-lg-6  page-list search-result-list pr-0" id="search-result-list">
+        <div className="col-xl-6  page-list search-result-list pr-0" id="search-result-list">
           <nav><SearchControl></SearchControl></nav>
           <nav><SearchControl></SearchControl></nav>
           <div className="d-flex align-items-start justify-content-between mt-1">
           <div className="d-flex align-items-start justify-content-between mt-1">
             <div className="search-result-meta">
             <div className="search-result-meta">
@@ -31,7 +31,7 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
             <ul className="page-list-ul page-list-ul-flat nav nav-pills"><SearchResultList></SearchResultList></ul>
             <ul className="page-list-ul page-list-ul-flat nav nav-pills"><SearchResultList></SearchResultList></ul>
           </div>
           </div>
         </div>
         </div>
-        <div className="col-lg-6 d-none d-lg-block search-result-content">
+        <div className="col-xl-6 d-none d-lg-block search-result-content">
           <SearchResultContent></SearchResultContent>
           <SearchResultContent></SearchResultContent>
         </div>
         </div>
       </div>
       </div>

+ 8 - 7
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -4,25 +4,26 @@ import RevisionLoader from '../Page/RevisionLoader';
 import AppContainer from '../../client/services/AppContainer';
 import AppContainer from '../../client/services/AppContainer';
 import SearchResultContentSubNavigation from './SearchResultContentSubNavigation';
 import SearchResultContentSubNavigation from './SearchResultContentSubNavigation';
 
 
+// TODO : set focusedPage type to ?IPageSearchResultData once #80214 is merged
+// PR: https://github.com/weseek/growi/pull/4649
+
 type Props ={
 type Props ={
   appContainer: AppContainer,
   appContainer: AppContainer,
   searchingKeyword:string,
   searchingKeyword:string,
-  focusedPage : any,
+  focusedPage: null | any,
 }
 }
+
+
 const SearchResultContent: FC<Props> = (props: Props) => {
 const SearchResultContent: FC<Props> = (props: Props) => {
   const page = props.focusedPage;
   const page = props.focusedPage;
   if (page == null) return null;
   if (page == null) return null;
-  // Temporaly workaround for lint error
-  // later needs to be fixed: RevisoinRender to typescriptcomponet
-  const RevisionLoaderTypeAny: any = RevisionLoader;
-  const SearchResultContentSubNavigationTypeAny: any = SearchResultContentSubNavigation;
   const growiRenderer = props.appContainer.getRenderer('searchresult');
   const growiRenderer = props.appContainer.getRenderer('searchresult');
   let showTags = false;
   let showTags = false;
   if (page.tags != null && page.tags.length > 0) { showTags = true }
   if (page.tags != null && page.tags.length > 0) { showTags = true }
   return (
   return (
     <div key={page._id} className="search-result-page mb-5">
     <div key={page._id} className="search-result-page mb-5">
-      <SearchResultContentSubNavigationTypeAny pageId={page._id} path={page.path}></SearchResultContentSubNavigationTypeAny>
-      <RevisionLoaderTypeAny
+      <SearchResultContentSubNavigation pageId={page._id} path={page.path}></SearchResultContentSubNavigation>
+      <RevisionLoader
         growiRenderer={growiRenderer}
         growiRenderer={growiRenderer}
         pageId={page._id}
         pageId={page._id}
         pagePath={page.path}
         pagePath={page.path}

+ 7 - 7
packages/app/src/components/SearchPage/SearchResultContentSubNavigation.tsx

@@ -6,6 +6,7 @@ import TagLabels from '../Page/TagLabels';
 import { toastSuccess, toastError } from '../../client/util/apiNotification';
 import { toastSuccess, toastError } from '../../client/util/apiNotification';
 import { apiPost } from '../../client/util/apiv1-client';
 import { apiPost } from '../../client/util/apiv1-client';
 import { useSWRTagsInfo } from '../../stores/page';
 import { useSWRTagsInfo } from '../../stores/page';
+import SubNavButtons from '../Navbar/SubNavButtons';
 
 
 type Props = {
 type Props = {
   appContainer:AppContainer
   appContainer:AppContainer
@@ -51,11 +52,7 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
       </div>
       </div>
       {/* Right side */}
       {/* Right side */}
       <div className="d-flex">
       <div className="d-flex">
-        {/* TODO: refactor SubNavButtons in a way that it can be used independently from pageContainer
-              TASK : #80481 https://estoc.weseek.co.jp/redmine/issues/80481
-              CONDITION reference: https://dev.growi.org/5fabddf8bbeb1a0048bcb9e9
-        */}
-        {/* <SubnavButtons isCompactMode={isCompactMode} /> */}
+        <SubNavButtons isCompactMode={isCompactMode} pageId={pageId}></SubNavButtons>
       </div>
       </div>
     </div>
     </div>
   );
   );
@@ -65,7 +62,10 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const SearchResultContentSubNavigationWrapper = withUnstatedContainers(SearchResultContentSubNavigation, [AppContainer]);
-
+const SearchResultContentSubNavigationUnstatedWrapper = withUnstatedContainers(SearchResultContentSubNavigation, [AppContainer]);
 
 
+// wrapping tsx component returned by withUnstatedContainers to avoid type error when this component used in other tsx components.
+const SearchResultContentSubNavigationWrapper = (props) => {
+  return <SearchResultContentSubNavigationUnstatedWrapper {...props}></SearchResultContentSubNavigationUnstatedWrapper>;
+};
 export default SearchResultContentSubNavigationWrapper;
 export default SearchResultContentSubNavigationWrapper;

+ 14 - 5
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -1,4 +1,7 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
+
+import Clamp from 'react-multiline-clamp';
+
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
@@ -85,7 +88,7 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
   };
   };
 
 
   return (
   return (
-    <li key={page._id} className={`page-list-li w-100 border-bottom pr-4 list-group-item-action ${isSelected ? 'active' : ''}`}>
+    <li key={page._id} className={`page-list-li search-page-item w-100 border-bottom pr-4 list-group-item-action ${isSelected ? 'active' : ''}`}>
       <a
       <a
         className="d-block pt-3"
         className="d-block pt-3"
         href={pageId}
         href={pageId}
@@ -117,13 +120,19 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
                 <PageItemControl page={page} />
                 <PageItemControl page={page} />
               </div>
               </div>
             </div>
             </div>
+            <div className="my-2">
+              <Clamp
+                lines={2}
+              >
+                {page.snippet
+                  ? <div className="mt-1">page.snippet</div>
+                  : <div className="mt-1" dangerouslySetInnerHTML={{ __html: page.elasticSearchResult.snippet }}></div>
+                }
+              </Clamp>
+            </div>
           </div>
           </div>
         </div>
         </div>
         {/* TODO: adjust snippet position */}
         {/* TODO: adjust snippet position */}
-        {page.snippet
-          ? <div className="mt-1">page.snippet</div>
-          : <div className="mt-1" dangerouslySetInnerHTML={{ __html: page.elasticSearchResult.snippet }}></div>
-        }
       </a>
       </a>
     </li>
     </li>
   );
   );

+ 8 - 0
packages/app/src/interfaces/page-info.ts

@@ -0,0 +1,8 @@
+export type IPageInfo = {
+  sumOfLikers: number;
+  likerIds: string[];
+  seenUserIds: string[];
+  sumOfSeenUsers: number;
+  isSeen: boolean;
+  isLiked: boolean;
+};

+ 2 - 2
packages/app/src/server/models/page.js

@@ -370,7 +370,7 @@ module.exports = function(crowi) {
       }
       }
       else {
       else {
         logger.debug('liker not updated');
         logger.debug('liker not updated');
-        return reject(self);
+        return reject(new Error('Already liked'));
       }
       }
     }));
     }));
   };
   };
@@ -391,7 +391,7 @@ module.exports = function(crowi) {
       }
       }
       else {
       else {
         logger.debug('liker not updated');
         logger.debug('liker not updated');
-        return reject(self);
+        return reject(new Error('Already unliked'));
       }
       }
     }));
     }));
   };
   };

+ 23 - 38
packages/app/src/server/service/slack-command-handler/note.js

@@ -1,8 +1,9 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const {
 const {
-  markdownSectionBlock, inputSectionBlock, inputBlock,
+  markdownHeaderBlock, inputSectionBlock, inputBlock, actionsBlock, buttonElement,
 } = require('@growi/slack');
 } = require('@growi/slack');
+const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 
 
 const logger = loggerFactory('growi:service:SlackCommandHandler:note');
 const logger = loggerFactory('growi:service:SlackCommandHandler:note');
 
 
@@ -14,58 +15,42 @@ module.exports = (crowi) => {
   const conversationsSelectElement = {
   const conversationsSelectElement = {
     action_id: 'conversation',
     action_id: 'conversation',
     type: 'conversations_select',
     type: 'conversations_select',
-    response_url_enabled: true,
     default_to_current_conversation: true,
     default_to_current_conversation: true,
   };
   };
 
 
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
-    await client.views.open({
-      trigger_id: body.trigger_id,
-
-      view: {
-        type: 'modal',
-        callback_id: 'note:createPage',
-        title: {
-          type: 'plain_text',
-          text: 'Take a note',
-        },
-        submit: {
-          type: 'plain_text',
-          text: 'Submit',
-        },
-        close: {
-          type: 'plain_text',
-          text: 'Cancel',
-        },
-        blocks: [
-          markdownSectionBlock('Take a note on GROWI'),
-          inputBlock(conversationsSelectElement, 'conversation', 'Channel name to display in the page to be created'),
-          inputSectionBlock('path', 'Page path', 'path_input', false, '/path'),
-          inputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
-        ],
-        private_metadata: JSON.stringify({ channelId: body.channel_id, channelName: body.channel_name }),
-      },
+    await respondUtil.respond({
+      text: 'Take a note on GROWI',
+      blocks: [
+        markdownHeaderBlock('Take a note on GROWI'),
+        inputBlock(conversationsSelectElement, 'conversation', 'Channel name to display in the page to be created'),
+        inputSectionBlock('path', 'Page path', 'path_input', false, '/path'),
+        inputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
+        actionsBlock(
+          buttonElement({ text: 'Cancel', actionId: 'note:cancel' }),
+          buttonElement({ text: 'Create page', actionId: 'note:createPage', style: 'primary' }),
+        ),
+
+      ],
     });
     });
   };
   };
 
 
+  handler.cancel = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
+    await respondUtil.deleteOriginal();
+  };
+
   handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName, respondUtil) {
   handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName, respondUtil) {
     await this[handlerMethodName](client, interactionPayload, interactionPayloadAccessor, respondUtil);
     await this[handlerMethodName](client, interactionPayload, interactionPayloadAccessor, respondUtil);
   };
   };
 
 
   handler.createPage = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
   handler.createPage = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
     const path = interactionPayloadAccessor.getStateValues()?.path.path_input.value;
     const path = interactionPayloadAccessor.getStateValues()?.path.path_input.value;
-    const privateMetadata = interactionPayloadAccessor.getViewPrivateMetaData();
-    if (privateMetadata == null) {
-      await respondUtil.respond({
-        text: 'Error occurred',
-        blocks: [
-          markdownSectionBlock('Failed to create a page.'),
-        ],
-      });
-      return;
-    }
     const contentsBody = interactionPayloadAccessor.getStateValues()?.contents.contents_input.value;
     const contentsBody = interactionPayloadAccessor.getStateValues()?.contents.contents_input.value;
+    if (path == null || contentsBody == null) {
+      throw new SlackCommandHandlerError('All parameters are required.');
+    }
     await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil);
     await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil);
+    await respondUtil.deleteOriginal();
   };
   };
 
 
   return handler;
   return handler;

+ 18 - 0
packages/app/src/stores/page.tsx

@@ -1,12 +1,18 @@
 import useSWR, { SWRResponse } from 'swr';
 import useSWR, { SWRResponse } from 'swr';
 
 
 import { apiv3Get } from '../client/util/apiv3-client';
 import { apiv3Get } from '../client/util/apiv3-client';
+<<<<<<< HEAD
 import { apiGet } from '../client/util/apiv1-client';
 import { apiGet } from '../client/util/apiv1-client';
 
 
 import { IPage } from '../interfaces/page';
 import { IPage } from '../interfaces/page';
 import { IPagingResult } from '../interfaces/paging-result';
 import { IPagingResult } from '../interfaces/paging-result';
 import { IPageTagsInfo } from '../interfaces/pageTagsInfo';
 import { IPageTagsInfo } from '../interfaces/pageTagsInfo';
+=======
+>>>>>>> feat/77524-search-result-conent-page
 
 
+import { IPage } from '../interfaces/page';
+import { IPagingResult } from '../interfaces/paging-result';
+import { IPageInfo } from '../interfaces/page-info';
 
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 export const useSWRxRecentlyUpdated = <Data, Error>(): SWRResponse<IPage[], Error> => {
 export const useSWRxRecentlyUpdated = <Data, Error>(): SWRResponse<IPage[], Error> => {
@@ -34,6 +40,18 @@ export const useSWRxPageList = (
   );
   );
 };
 };
 
 
+export const useSWRPageInfo = (pageId: string): SWRResponse<IPageInfo, Error> => {
+  return useSWR(`/page/info?pageId=${pageId}`, endpoint => apiv3Get(endpoint).then((response) => {
+    return {
+      sumOfLikers: response.data.sumOfLikers,
+      likerIds: response.data.likerIds,
+      seenUserIds: response.data.seenUserIds,
+      sumOfSeenUsers: response.data.sumOfSeenUsers,
+      isSeen: response.data.isSeen,
+      isLiked: response.data?.isLiked,
+    };
+  }));
+};
 
 
 export const useSWRTagsInfo = (pageId: string): SWRResponse<IPageTagsInfo, Error> => {
 export const useSWRTagsInfo = (pageId: string): SWRResponse<IPageTagsInfo, Error> => {
   // apiGet():Promise<unknown>
   // apiGet():Promise<unknown>

+ 4 - 0
packages/app/src/styles/_search.scss

@@ -245,6 +245,10 @@
   }
   }
 }
 }
 
 
+.search-page-item {
+  height: 130px;
+}
+
 @include media-breakpoint-down(sm) {
 @include media-breakpoint-down(sm) {
   .grw-search-table {
   .grw-search-table {
     th {
     th {

+ 5 - 0
yarn.lock

@@ -16860,6 +16860,11 @@ react-motion@^0.5.0, react-motion@^0.5.2:
     prop-types "^15.5.8"
     prop-types "^15.5.8"
     raf "^3.1.0"
     raf "^3.1.0"
 
 
+react-multiline-clamp@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/react-multiline-clamp/-/react-multiline-clamp-2.0.0.tgz#913a2092368ef1b52c1c79364d506ba4af27e019"
+  integrity sha512-iPm3HxFD6LO63lE5ZnThiqs+6A3c+LW3WbsEM0oa0iNTa0qN4SKx/LK/6ZToSmXundEcQXBFVNzKDvgmExawTw==
+
 react-node-resolver@^1.0.1:
 react-node-resolver@^1.0.1:
   version "1.0.1"
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/react-node-resolver/-/react-node-resolver-1.0.1.tgz#1798a729c0e218bf2f0e8ddf79c550d4af61d83a"
   resolved "https://registry.yarnpkg.com/react-node-resolver/-/react-node-resolver-1.0.1.tgz#1798a729c0e218bf2f0e8ddf79c550d4af61d83a"