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

Merge branch 'feat/77515-display-search-result-with-snippet' into feat/77515-79012-fix-bug-caused-by-search-refactor

* feat/77515-display-search-result-with-snippet:
  move almost all codes into SearchResultListItem component and remove the rest of unnecessary codes
  77750 add logger
  77750 create SearchResultListItem component and modify layout
  77750 add isFormerOnly prop to PagePathLabel
  modify nullable
  add i18n
  add nullable check
  modify naming and delete path file
  modify comment
  modify comment
  change branch on condition
  remove interface
  delete file
  modify path name and prefix
  implement search function with prefix
  mofidy one line
  change search box in search result page
Mao 4 лет назад
Родитель
Сommit
928a6ac8f4

+ 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) {
@@ -97,6 +127,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);
@@ -152,6 +190,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}

+ 6 - 66
packages/app/src/components/SearchPage/SearchResultList.jsx

@@ -1,82 +1,23 @@
 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) => {
-      // 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}`;
       return (
       return (
-        <li key={page._id} className="nav-item page-list-li w-100 m-0 border-bottom">
-          <a
-            className="nav-link page-list-link d-flex align-items-baseline"
-            href={pageId}
-            onClick={() => {
-              try {
-                if (this.props.onClickInvoked == null) { throw new Error('onClickInvoked is null') }
-                this.props.onClickInvoked(page._id);
-              }
-              catch (error) {
-                logger.error(error);
-              }
-            }}
-          >
-            <div className="form-check my-auto">
-              <input className="form-check-input my-auto" type="checkbox" value="" id="flexCheckDefault" />
-            </div>
-            {/* TODO: remove dummy snippet and adjust style */}
-            <div className="d-block">
-              <Page page={page} noLink />
-              <div className="border-gray mt-5">{page.snippet}</div>
-            </div>
-            <div className="ml-auto d-flex">
-              {this.props.deletionMode && (
-                <div className="custom-control custom-checkbox custom-checkbox-danger">
-                  <input
-                    type="checkbox"
-                    id={`page-delete-check-${page._id}`}
-                    className="custom-control-input search-result-list-delete-checkbox"
-                    value={pageId}
-                    checked={this.props.selectedPages.has(page)}
-                    onChange={() => {
-                      try {
-                        if (this.props.onChangeInvoked == null) { throw new Error('onChnageInvoked is null') }
-                        return this.props.onChangeInvoked(page);
-                      }
-                      catch (error) {
-                        logger.error(error);
-                      }
-                    }}
-                  />
-                  <label className="custom-control-label" htmlFor={`page-delete-check-${page._id}`}></label>
-                </div>
-              )}
-              <div className="page-list-option">
-                <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>
-          </a>
-        </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,
@@ -85,5 +26,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),
 };
 };