Răsfoiți Sursa

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 ani în urmă
părinte
comite
baf24f8fd9

+ 1 - 0
packages/app/package.json

@@ -133,6 +133,7 @@
     "prom-client": "^13.0.0",
     "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
+    "react-multiline-clamp": "^2.0.0",
     "reconnecting-websocket": "^4.4.0",
     "redis": "^3.0.2",
     "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();
   }
 
-  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() {
     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 { pathUtils } from '@growi/core';
 import urljoin from 'url-join';
-import { withUnstatedContainers } from './UnstatedUtils';
 
-import PageContainer from '~/client/services/PageContainer';
 
 const CreateTemplateModal = (props) => {
-  const { t, pageContainer } = props;
+  const { t, path } = props;
 
-  const { path } = pageContainer.state;
   const parentPath = pathUtils.addTrailingSlash(path);
 
   function generateUrl(label) {
@@ -67,18 +64,11 @@ const CreateTemplateModal = (props) => {
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const CreateTemplateModalWrapper = withUnstatedContainers(CreateTemplateModal, [PageContainer]);
-
-
 CreateTemplateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-
+  path: PropTypes.string.isRequired,
   isOpen: PropTypes.bool.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 { withUnstatedContainers } from './UnstatedUtils';
 
-import { toastError } from '~/client/util/apiNotification';
 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) {
     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;
     }
-
-    try {
-      pageContainer.toggleLike();
-    }
-    catch (err) {
-      toastError(err);
-    }
+    this.props.onLikeClicked();
   }
 
   render() {
-    const { appContainer, pageContainer, t } = this.props;
-    const { isGuestUser } = appContainer;
     const {
-      state: { likers, sumOfLikers, isLiked },
-    } = pageContainer;
+      appContainer, likerIds, sumOfLikers, isLiked, t,
+    } = this.props;
+    const { isGuestUser } = appContainer;
 
     return (
       <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">
           <PopoverBody className="seen-user-popover">
             <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>
           </PopoverBody>
         </Popover>
@@ -89,14 +80,20 @@ class LikeButtons extends React.Component {
 /**
  * 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,
-  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,
-  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 TagLabels from '../Page/TagLabels';
-import SubnavButtons from './SubNavButtons';
+import SubNavButtons from './SubNavButtons';
 import PageEditorModeManager from './PageEditorModeManager';
 
 import AuthorInfo from './AuthorInfo';
@@ -84,7 +84,7 @@ const GrowiSubNavigation = (props) => {
 
         <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
-            <SubnavButtons isCompactMode={isCompactMode} />
+            <SubNavButtons isCompactMode={isCompactMode} pageId={pageId} />
           </div>
           <div className="mt-2">
             {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}
         />
         <CreateTemplateModal
+          path={path}
           isOpen={isPageTemplateModalShown}
           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
  */
-class RevisionLoader extends React.Component {
+class LegacyRevisionLoader extends React.Component {
 
   constructor(props) {
     super(props);
@@ -116,9 +116,9 @@ class RevisionLoader extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const RevisionLoaderWrapper = withUnstatedContainers(RevisionLoader, [AppContainer]);
+const LegacyRevisionLoaderWrapper = withUnstatedContainers(LegacyRevisionLoader, [AppContainer]);
 
-RevisionLoader.propTypes = {
+LegacyRevisionLoader.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   growiRenderer: PropTypes.instanceOf(GrowiRenderer).isRequired,
@@ -129,4 +129,7 @@ RevisionLoader.propTypes = {
   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: '',
       searchedPages: [],
       searchResultMeta: {},
-      focusedPage: {},
+      focusedPage: null,
       selectedPages: new Set(),
       searchResultCount: 0,
       activePage: 1,
-      pagingLimit: 3, // change to an appropriate limit number
+      pagingLimit: 10, // change to an appropriate limit number
       excludeUsersHome: true,
       excludeTrash: true,
     };
@@ -163,7 +163,7 @@ class SearchPage extends React.Component {
           searchedPages: [],
           searchResultMeta: {},
           searchResultCount: 0,
-          focusedPage: {},
+          focusedPage: null,
           activePage: 1,
         });
       }

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

@@ -19,7 +19,7 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
   return (
     <div className="content-main">
       <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>
           <div className="d-flex align-items-start justify-content-between mt-1">
             <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>
           </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>
         </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 SearchResultContentSubNavigation from './SearchResultContentSubNavigation';
 
+// TODO : set focusedPage type to ?IPageSearchResultData once #80214 is merged
+// PR: https://github.com/weseek/growi/pull/4649
+
 type Props ={
   appContainer: AppContainer,
   searchingKeyword:string,
-  focusedPage : any,
+  focusedPage: null | any,
 }
+
+
 const SearchResultContent: FC<Props> = (props: Props) => {
   const page = props.focusedPage;
   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');
   let showTags = false;
   if (page.tags != null && page.tags.length > 0) { showTags = true }
   return (
     <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}
         pageId={page._id}
         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 { apiPost } from '../../client/util/apiv1-client';
 import { useSWRTagsInfo } from '../../stores/page';
+import SubNavButtons from '../Navbar/SubNavButtons';
 
 type Props = {
   appContainer:AppContainer
@@ -51,11 +52,7 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
       </div>
       {/* Right side */}
       <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>
   );
@@ -65,7 +62,10 @@ const SearchResultContentSubNavigation: FC<Props> = (props : Props) => {
 /**
  * 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;

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

@@ -1,4 +1,7 @@
 import React, { FC } from 'react';
+
+import Clamp from 'react-multiline-clamp';
+
 import { useTranslation } from 'react-i18next';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
@@ -85,7 +88,7 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
   };
 
   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
         className="d-block pt-3"
         href={pageId}
@@ -117,13 +120,19 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
                 <PageItemControl page={page} />
               </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>
         {/* TODO: adjust snippet position */}
-        {page.snippet
-          ? <div className="mt-1">page.snippet</div>
-          : <div className="mt-1" dangerouslySetInnerHTML={{ __html: page.elasticSearchResult.snippet }}></div>
-        }
       </a>
     </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 {
         logger.debug('liker not updated');
-        return reject(self);
+        return reject(new Error('Already liked'));
       }
     }));
   };
@@ -391,7 +391,7 @@ module.exports = function(crowi) {
       }
       else {
         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';
 
 const {
-  markdownSectionBlock, inputSectionBlock, inputBlock,
+  markdownHeaderBlock, inputSectionBlock, inputBlock, actionsBlock, buttonElement,
 } = require('@growi/slack');
+const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 
 const logger = loggerFactory('growi:service:SlackCommandHandler:note');
 
@@ -14,58 +15,42 @@ module.exports = (crowi) => {
   const conversationsSelectElement = {
     action_id: 'conversation',
     type: 'conversations_select',
-    response_url_enabled: true,
     default_to_current_conversation: true,
   };
 
   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) {
     await this[handlerMethodName](client, interactionPayload, interactionPayloadAccessor, respondUtil);
   };
 
   handler.createPage = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
     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;
+    if (path == null || contentsBody == null) {
+      throw new SlackCommandHandlerError('All parameters are required.');
+    }
     await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil);
+    await respondUtil.deleteOriginal();
   };
 
   return handler;

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

@@ -1,12 +1,18 @@
 import useSWR, { SWRResponse } from 'swr';
 
 import { apiv3Get } from '../client/util/apiv3-client';
+<<<<<<< HEAD
 import { apiGet } from '../client/util/apiv1-client';
 
 import { IPage } from '../interfaces/page';
 import { IPagingResult } from '../interfaces/paging-result';
 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
 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> => {
   // 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) {
   .grw-search-table {
     th {

+ 5 - 0
yarn.lock

@@ -16860,6 +16860,11 @@ react-motion@^0.5.0, react-motion@^0.5.2:
     prop-types "^15.5.8"
     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:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/react-node-resolver/-/react-node-resolver-1.0.1.tgz#1798a729c0e218bf2f0e8ddf79c550d4af61d83a"