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

Merge branch 'feat/77515-display-search-result-with-snippet' into feat/77515-78577-show-snippet-in-search-result

# Conflicts:
#	packages/app/src/components/SearchPage/SearchResultList.jsx
SULLEY\ryo-h 4 лет назад
Родитель
Сommit
6d65712aa6

+ 1 - 0
packages/app/config/logger/config.dev.js

@@ -35,5 +35,6 @@ module.exports = {
   'growi:services:*': 'debug',
   'growi:services:*': 'debug',
   // 'growi:StaffCredit': 'debug',
   // 'growi:StaffCredit': 'debug',
   // 'growi:cli:StickyStretchableScroller': 'debug',
   // 'growi:cli:StickyStretchableScroller': 'debug',
+  'growi:searchResultList': 'debug',
 
 
 };
 };

+ 1 - 0
packages/app/resource/locales/en_US/translation.json

@@ -63,6 +63,7 @@
   "Include Attachment File": "Include Attachment File",
   "Include Attachment File": "Include Attachment File",
   "Include Comment": "Include Comment",
   "Include Comment": "Include Comment",
   "Include Subordinated Page": "Include Subordinated Page",
   "Include Subordinated Page": "Include Subordinated Page",
+  "Include Subordinated Target Page": "include {{target}}",
   "All Subordinated Page": "All Subordinated Page",
   "All Subordinated Page": "All Subordinated Page",
   "Specify Hierarchy": "Specify Hierarchy",
   "Specify Hierarchy": "Specify Hierarchy",
   "Submitted the request to create the archive": "Submitted the request to create the archive",
   "Submitted the request to create the archive": "Submitted the request to create the archive",

+ 1 - 0
packages/app/resource/locales/ja_JP/translation.json

@@ -64,6 +64,7 @@
   "Include Attachment File": "添付ファイルも含める",
   "Include Attachment File": "添付ファイルも含める",
   "Include Comment": "コメントも含める",
   "Include Comment": "コメントも含める",
   "Include Subordinated Page": "配下ページも含める",
   "Include Subordinated Page": "配下ページも含める",
+  "Include Subordinated Target Page": "{{target}}下を含む",
   "All Subordinated Page": "全ての配下ページ",
   "All Subordinated Page": "全ての配下ページ",
   "Specify Hierarchy": "階層の深さを指定",
   "Specify Hierarchy": "階層の深さを指定",
   "Submitted the request to create the archive": "アーカイブ作成のリクエストを正常に送信しました",
   "Submitted the request to create the archive": "アーカイブ作成のリクエストを正常に送信しました",

+ 1 - 0
packages/app/resource/locales/zh_CN/translation.json

@@ -65,6 +65,7 @@
   "Include Attachment File": "包含附件",
   "Include Attachment File": "包含附件",
   "Include Comment": "包含评论",
   "Include Comment": "包含评论",
   "Include Subordinated Page": "包括子页面",
   "Include Subordinated Page": "包括子页面",
+  "Include Subordinated Target Page": "包括 {{target}}",
   "All Subordinated Page": "所有子页面",
   "All Subordinated Page": "所有子页面",
   "Specify Hierarchy": "指定层级",
   "Specify Hierarchy": "指定层级",
   "Submitted the request to create the archive": "提交创建归档请求",
   "Submitted the request to create the archive": "提交创建归档请求",

+ 41 - 1
packages/app/src/components/SearchPage.jsx

@@ -13,6 +13,11 @@ import SearchResultContent from './SearchPage/SearchResultContent';
 import SearchResultList from './SearchPage/SearchResultList';
 import SearchResultList from './SearchPage/SearchResultList';
 import SearchControl from './SearchPage/SearchControl';
 import SearchControl from './SearchPage/SearchControl';
 
 
+export const specificPathNames = {
+  user: '/user',
+  trash: '/trash',
+};
+
 class SearchPage extends React.Component {
 class SearchPage extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -27,12 +32,16 @@ class SearchPage extends React.Component {
       searchResultMeta: {},
       searchResultMeta: {},
       selectedPage: {},
       selectedPage: {},
       selectedPages: new Set(),
       selectedPages: new Set(),
+      excludeUsersHome: true,
+      excludeTrash: true,
     };
     };
 
 
     this.changeURL = this.changeURL.bind(this);
     this.changeURL = this.changeURL.bind(this);
     this.search = this.search.bind(this);
     this.search = this.search.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
+    this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
+    this.onExcludeTrash = this.onExcludeTrash.bind(this);
   }
   }
 
 
   componentDidMount() {
   componentDidMount() {
@@ -54,6 +63,14 @@ class SearchPage extends React.Component {
     return query;
     return query;
   }
   }
 
 
+  onExcludeUsersHome() {
+    this.setState({ excludeUsersHome: !this.state.excludeUsersHome });
+  }
+
+  onExcludeTrash() {
+    this.setState({ excludeTrash: !this.state.excludeTrash });
+  }
+
   changeURL(keyword, refreshHash) {
   changeURL(keyword, refreshHash) {
     let hash = window.location.hash || '';
     let hash = window.location.hash || '';
     // TODO 整理する
     // TODO 整理する
@@ -65,6 +82,19 @@ class SearchPage extends React.Component {
     }
     }
   }
   }
 
 
+  createSearchQuery(keyword) {
+    let query = keyword;
+
+    // pages included in specific path are not retrived when prefix is added
+    if (this.state.excludeTrash) {
+      query = `${query} -prefix:${specificPathNames.trash}`;
+    }
+    if (this.state.excludeUsersHome) {
+      query = `${query} -prefix:${specificPathNames.user}`;
+    }
+
+    return query;
+  }
 
 
   search(data) {
   search(data) {
     const keyword = data.keyword;
     const keyword = data.keyword;
@@ -81,7 +111,7 @@ class SearchPage extends React.Component {
     this.setState({
     this.setState({
       searchingKeyword: keyword,
       searchingKeyword: keyword,
     });
     });
-    this.props.appContainer.apiGet('/search', { q: keyword })
+    this.props.appContainer.apiGet('/search', { q: this.createSearchQuery(keyword) })
       .then((res) => {
       .then((res) => {
         this.changeURL(keyword);
         this.changeURL(keyword);
         if (res.data.length > 0) {
         if (res.data.length > 0) {
@@ -92,6 +122,14 @@ class SearchPage extends React.Component {
             selectedPage: res.data[0],
             selectedPage: res.data[0],
           });
           });
         }
         }
+        else {
+          this.setState({
+            searchedKeyword: keyword,
+            searchedPages: [],
+            searchResultMeta: {},
+            selectedPage: {},
+          });
+        }
       })
       })
       .catch((err) => {
       .catch((err) => {
         toastError(err);
         toastError(err);
@@ -147,6 +185,8 @@ class SearchPage extends React.Component {
         searchingKeyword={this.state.searchingKeyword}
         searchingKeyword={this.state.searchingKeyword}
         appContainer={this.props.appContainer}
         appContainer={this.props.appContainer}
         onSearchInvoked={this.search}
         onSearchInvoked={this.search}
+        onExcludeUsersHome={this.onExcludeUsersHome}
+        onExcludeTrash={this.onExcludeTrash}
       >
       >
       </SearchControl>
       </SearchControl>
     );
     );

+ 44 - 2
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -1,18 +1,34 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
+import { useTranslation } from 'react-i18next';
 import SearchPageForm from './SearchPageForm';
 import SearchPageForm from './SearchPageForm';
 import AppContainer from '../../client/services/AppContainer';
 import AppContainer from '../../client/services/AppContainer';
 
 
-
 type Props = {
 type Props = {
   searchingKeyword: string,
   searchingKeyword: string,
   appContainer: AppContainer,
   appContainer: AppContainer,
   onSearchInvoked: (data : any[]) => boolean,
   onSearchInvoked: (data : any[]) => boolean,
+  onExcludeUsersHome?: () => void,
+  onExcludeTrash?: () => void,
 }
 }
 
 
 const SearchControl: FC <Props> = (props: Props) => {
 const SearchControl: FC <Props> = (props: Props) => {
   // Temporaly workaround for lint error
   // Temporaly workaround for lint error
   // later needs to be fixed: SearchControl to typescript componet
   // later needs to be fixed: SearchControl to typescript componet
   const SearchPageFormTypeAny : any = SearchPageForm;
   const SearchPageFormTypeAny : any = SearchPageForm;
+  const { t } = useTranslation('');
+
+  const onExcludeUsersHome = () => {
+    if (props.onExcludeUsersHome != null) {
+      props.onExcludeUsersHome();
+    }
+  };
+
+  const onExcludeTrash = () => {
+    if (props.onExcludeTrash != null) {
+      props.onExcludeTrash();
+    }
+  };
+
   return (
   return (
     <div className="">
     <div className="">
       <div className="search-page-input sps sps--abv">
       <div className="search-page-input sps sps--abv">
@@ -22,7 +38,33 @@ const SearchControl: FC <Props> = (props: Props) => {
           onSearchFormChanged={props.onSearchInvoked}
           onSearchFormChanged={props.onSearchInvoked}
         />
         />
       </div>
       </div>
-      {/* TODO: place deleteAll button , relevance button , include specificPath button */}
+      {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
+      <div className="d-flex my-4">
+        <div className="form-check border-gray">
+          <input
+            className="form-check-input"
+            type="checkbox"
+            value=""
+            id="flexCheckDefault"
+            onClick={() => onExcludeUsersHome()}
+          />
+          <label className="form-check-label" htmlFor="flexCheckDefault">
+            {t('Include Subordinated Target Page', { target: '/user' })}
+          </label>
+        </div>
+        <div className="form-check">
+          <input
+            className="form-check-input"
+            type="checkbox"
+            value=""
+            id="flexCheckChecked"
+            onClick={() => onExcludeTrash()}
+          />
+          <label className="form-check-label" htmlFor="flexCheckChecked">
+            {t('Include Subordinated Target Page', { target: '/trash' })}
+          </label>
+        </div>
+      </div>
     </div>
     </div>
   );
   );
 };
 };

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

@@ -40,8 +40,9 @@ class SearchPageForm extends React.Component {
 
 
   render() {
   render() {
     return (
     return (
-      <div className="input-group mb-3 d-flex">
-        <div className="flex-fill">
+      // TODO: modify design after other component is created
+      <div className="grw-search-form-in-search-result-page d-flex">
+        <div className="input-group flex-nowrap">
           <SearchForm
           <SearchForm
             onSubmit={this.search}
             onSubmit={this.search}
             keyword={this.state.searchedKeyword}
             keyword={this.state.searchedKeyword}

+ 7 - 7
packages/app/src/components/SearchPage/SearchResultList.jsx

@@ -1,14 +1,11 @@
-import React from 'react';
+import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
-import Page from '../PageList/Page';
-import loggerFactory from '~/utils/logger';
+import SearchResultListItem from './SearchResultListItem';
 
 
-const logger = loggerFactory('growi:searchResultList');
 class SearchResultList extends React.Component {
 class SearchResultList extends React.Component {
 
 
   render() {
   render() {
     return this.props.pages.map((page) => {
     return this.props.pages.map((page) => {
-      const pageId = `#${page._id}`;
       // TODO : send cetain  length of body (revisionBody) from elastisearch by adding some settings to the query and
       // TODO : send cetain  length of body (revisionBody) from elastisearch by adding some settings to the query and
       //         when keyword is not in page content, display revisionBody.
       //         when keyword is not in page content, display revisionBody.
       // TASK : https://estoc.weseek.co.jp/redmine/issues/79606
       // TASK : https://estoc.weseek.co.jp/redmine/issues/79606
@@ -74,13 +71,17 @@ class SearchResultList extends React.Component {
             </div>
             </div>
           </a>
           </a>
         </li>
         </li>
+        <SearchResultListItem
+          page={page}
+          onClickInvoked={this.props.onClickInvoked}
+          noLink
+        />
       );
       );
     });
     });
   }
   }
 
 
 }
 }
 
 
-
 SearchResultList.propTypes = {
 SearchResultList.propTypes = {
   pages: PropTypes.array.isRequired,
   pages: PropTypes.array.isRequired,
   deletionMode: PropTypes.bool.isRequired,
   deletionMode: PropTypes.bool.isRequired,
@@ -89,5 +90,4 @@ SearchResultList.propTypes = {
   onChangeInvoked: PropTypes.func,
   onChangeInvoked: PropTypes.func,
 };
 };
 
 
-
 export default SearchResultList;
 export default SearchResultList;

+ 92 - 0
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -0,0 +1,92 @@
+import React, { FC } from 'react';
+
+import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
+import { DevidedPagePath } from '@growi/core';
+
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:searchResultList');
+
+type Props ={
+  page: {
+    _id: string,
+    snippet: string,
+    path: string,
+    noLink: boolean,
+    lastUpdateUser: any
+  },
+  onClickInvoked: (data: string) => void,
+}
+
+const SearchResultListItem: FC<Props> = (props:Props) => {
+
+  const { page, onClickInvoked } = props;
+
+  // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
+  const pageId = `#${page._id}`;
+
+  const dPagePath = new DevidedPagePath(page.path, false, true);
+  const pagePathElem = <PagePathLabel page={page} isFormerOnly />;
+
+  return (
+    <li key={page._id} className="page-list-li w-100 border-bottom pr-4">
+      <a
+        className="d-block pt-3"
+        href={pageId}
+        onClick={() => {
+          try {
+            if (onClickInvoked == null) { throw new Error('onClickInvoked is null') }
+            onClickInvoked(page._id);
+          }
+          catch (error) {
+            logger.error(error);
+          }
+        }}
+      >
+        <div className="d-flex">
+          {/* checkbox */}
+          <div className="form-check my-auto mx-2">
+            <input className="form-check-input my-auto" type="checkbox" value="" id="flexCheckDefault" />
+          </div>
+          <div className="w-100">
+            {/* page path */}
+            <small className="mb-1">
+              <i className="icon-fw icon-home"></i>
+              {pagePathElem}
+            </small>
+            <div className="d-flex my-1 align-items-center">
+              {/* page title */}
+              <h3 className="mb-0">
+                <UserPicture user={page.lastUpdateUser} />
+                <span className="mx-2">{dPagePath.latter}</span>
+              </h3>
+              {/* page meta */}
+              <div className="d-flex mx-2">
+                <PageListMeta page={page} />
+              </div>
+              {/* doropdown icon */}
+              <div className="ml-auto">
+                <i className="fa fa-ellipsis-v text-muted"></i>
+              </div>
+
+              {/* Todo: add the following icon into dropdown menu */}
+              {/* <button
+                type="button"
+                className="btn btn-link p-0"
+                value={page.path}
+                onClick={(e) => {
+                  window.location.href = e.currentTarget.value;
+                }}
+              >
+                <i className="icon-login" />
+              </button> */}
+
+            </div>
+            <div className="mt-1">{page.snippet}</div>
+          </div>
+        </div>
+      </a>
+    </li>
+  );
+};
+export default SearchResultListItem;

+ 8 - 0
packages/ui/src/components/PagePath/PagePathLabel.jsx

@@ -14,6 +14,13 @@ export const PagePathLabel = (props) => {
     return <span className={classNames.join(' ')}>{dPagePath.latter}</span>;
     return <span className={classNames.join(' ')}>{dPagePath.latter}</span>;
   }
   }
 
 
+  if (props.isFormerOnly) {
+    const textElem = dPagePath.isFormerRoot
+      ? <>/</>
+      : <>{dPagePath.former}</>;
+    return <span className={classNames.join(' ')}>{textElem}</span>;
+  }
+
   const textElem = dPagePath.isRoot
   const textElem = dPagePath.isRoot
     ? <><strong>/</strong></>
     ? <><strong>/</strong></>
     : <>{dPagePath.former}/<strong>{dPagePath.latter}</strong></>;
     : <>{dPagePath.former}/<strong>{dPagePath.latter}</strong></>;
@@ -24,6 +31,7 @@ export const PagePathLabel = (props) => {
 PagePathLabel.propTypes = {
 PagePathLabel.propTypes = {
   page: PropTypes.object.isRequired,
   page: PropTypes.object.isRequired,
   isLatterOnly: PropTypes.bool,
   isLatterOnly: PropTypes.bool,
+  isFormerOnly: PropTypes.bool,
   additionalClassNames: PropTypes.arrayOf(PropTypes.string),
   additionalClassNames: PropTypes.arrayOf(PropTypes.string),
 };
 };