Răsfoiți Sursa

Merge branch 'feat/search-implement' into feat/80324-adjust-design-for-left-pane

Yohei-Shiina 4 ani în urmă
părinte
comite
119e4616cd

+ 6 - 6
packages/app/src/components/PaginationWrapper.jsx

@@ -63,10 +63,10 @@ const PaginationWrapper = React.memo((props) => {
     if (activePage !== 1) {
       paginationItems.push(
         <PaginationItem key="painationItemFirst">
-          <PaginationLink first onClick={() => { return changePage(1) }} />
+          <PaginationLink first onClick={() => { return changePage != null && changePage(1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemPrevious">
-          <PaginationLink previous onClick={() => { return changePage(activePage - 1) }} />
+          <PaginationLink previous onClick={() => { return changePage != null && changePage(activePage - 1) }} />
         </PaginationItem>,
       );
     }
@@ -93,7 +93,7 @@ const PaginationWrapper = React.memo((props) => {
     for (let number = paginationStart; number <= maxViewPageNum; number++) {
       paginationItems.push(
         <PaginationItem key={`paginationItem-${number}`} active={number === activePage}>
-          <PaginationLink onClick={() => { return changePage(number) }}>
+          <PaginationLink onClick={() => { return changePage != null && changePage(number) }}>
             {number}
           </PaginationLink>
         </PaginationItem>,
@@ -112,10 +112,10 @@ const PaginationWrapper = React.memo((props) => {
     if (totalPage !== activePage) {
       paginationItems.push(
         <PaginationItem key="painationItemNext">
-          <PaginationLink next onClick={() => { return changePage(activePage + 1) }} />
+          <PaginationLink next onClick={() => { return changePage != null && changePage(activePage + 1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemLast">
-          <PaginationLink last onClick={() => { return changePage(totalPage) }} />
+          <PaginationLink last onClick={() => { return changePage != null && changePage(totalPage) }} />
         </PaginationItem>,
       );
     }
@@ -159,7 +159,7 @@ const PaginationWrapper = React.memo((props) => {
 
 PaginationWrapper.propTypes = {
   activePage: PropTypes.number.isRequired,
-  changePage: PropTypes.func.isRequired,
+  changePage: PropTypes.func,
   totalItemsCount: PropTypes.number.isRequired,
   pagingLimit: PropTypes.number,
   align: PropTypes.string,

+ 66 - 26
packages/app/src/components/SearchPage.jsx

@@ -32,16 +32,21 @@ class SearchPage extends React.Component {
       searchResultMeta: {},
       selectedPage: {},
       selectedPages: new Set(),
+      searchResultCount: 0,
+      activePage: 1,
+      pagingLimit: 3, // change to an appropriate limit number
       excludeUsersHome: true,
       excludeTrash: true,
     };
 
     this.changeURL = this.changeURL.bind(this);
     this.search = this.search.bind(this);
+    this.searchHandler = this.searchHandler.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
     this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
     this.onExcludeTrash = this.onExcludeTrash.bind(this);
+    this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
   }
 
   componentDidMount() {
@@ -96,13 +101,34 @@ class SearchPage extends React.Component {
     return query;
   }
 
-  search(data) {
+  /**
+   * this method is called when user changes paging number
+   */
+  async onPagingNumberChanged(activePage) {
+    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
+    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
+    this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
+  }
+
+  /**
+   * this method is called when user searches by pressing Enter or using searchbox
+   */
+  async searchHandler(data) {
+    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
+    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
+    this.setState({ activePage: 1 }, () => this.search(data));
+  }
+
+  async search(data) {
     const keyword = data.keyword;
     if (keyword === '') {
       this.setState({
         searchingKeyword: '',
+        searchedKeyword: '',
         searchedPages: [],
         searchResultMeta: {},
+        searchResultCount: 0,
+        activePage: 1,
       });
 
       return true;
@@ -111,29 +137,40 @@ class SearchPage extends React.Component {
     this.setState({
       searchingKeyword: keyword,
     });
-    this.props.appContainer.apiGet('/search', { q: this.createSearchQuery(keyword) })
-      .then((res) => {
-        this.changeURL(keyword);
-        if (res.data.length > 0) {
-          this.setState({
-            searchedKeyword: keyword,
-            searchedPages: res.data,
-            searchResultMeta: res.meta,
-            selectedPage: res.data[0],
-          });
-        }
-        else {
-          this.setState({
-            searchedKeyword: keyword,
-            searchedPages: [],
-            searchResultMeta: {},
-            selectedPage: {},
-          });
-        }
-      })
-      .catch((err) => {
-        toastError(err);
+    const pagingLimit = this.state.pagingLimit;
+    const offset = (this.state.activePage * pagingLimit) - pagingLimit;
+    try {
+      const res = await this.props.appContainer.apiGet('/search', {
+        q: this.createSearchQuery(keyword),
+        limit: pagingLimit,
+        offset,
       });
+      this.changeURL(keyword);
+      if (res.data.length > 0) {
+        this.setState({
+          searchedKeyword: keyword,
+          searchedPages: res.data,
+          searchResultMeta: res.meta,
+          searchResultCount: res.meta.total,
+          selectedPage: res.data[0],
+          // reset active page if keyword changes, otherwise set the current state
+          activePage: this.state.searchedKeyword === keyword ? this.state.activePage : 1,
+        });
+      }
+      else {
+        this.setState({
+          searchedKeyword: keyword,
+          searchedPages: [],
+          searchResultMeta: {},
+          searchResultCount: 0,
+          selectedPage: {},
+          activePage: 1,
+        });
+      }
+    }
+    catch (err) {
+      toastError(err);
+    }
   }
 
   selectPage= (pageId) => {
@@ -172,10 +209,13 @@ class SearchPage extends React.Component {
         deletionMode={false}
         selectedPage={this.state.selectedPage}
         selectedPages={this.state.selectedPages}
+        searchResultCount={this.state.searchResultCount}
+        activePage={this.state.activePage}
+        pagingLimit={this.state.pagingLimit}
         onClickInvoked={this.selectPage}
         onChangedInvoked={this.toggleCheckBox}
-      >
-      </SearchResultList>
+        onPagingNumberChanged={this.onPagingNumberChanged}
+      />
     );
   }
 
@@ -184,7 +224,7 @@ class SearchPage extends React.Component {
       <SearchControl
         searchingKeyword={this.state.searchingKeyword}
         appContainer={this.props.appContainer}
-        onSearchInvoked={this.search}
+        onSearchInvoked={this.searchHandler}
         onExcludeUsersHome={this.onExcludeUsersHome}
         onExcludeTrash={this.onExcludeTrash}
       >

+ 0 - 39
packages/app/src/components/SearchPage/DeleteAllButton.jsx

@@ -1,39 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { useTranslation } from 'react-i18next';
-
-const DeleteAllButton = (props) => {
-  const { selectedPage, checked } = props;
-  const { t } = useTranslation();
-  function deleteAllSelectedPage(pagesToDelete) {
-    // TODO: implement this function
-    // https://estoc.weseek.co.jp/redmine/issues/77543
-    // do something with pagesDelete to delete them.
-  }
-  return (
-    <div>
-      <label>
-        <input
-          type="checkbox"
-          name="check-delte-all"
-          onChange={() => {
-            if (checked) {
-              deleteAllSelectedPage(selectedPage);
-            }
-          }}
-        />
-        <span className="text-danger font-weight-light">
-          <i className="icon-trash ml-3"></i>
-          {t('search_result.delete_all_selected_page')}
-        </span>
-      </label>
-    </div>
-  );
-
-};
-
-DeleteAllButton.propTypes = {
-  selectedPage: PropTypes.array.isRequired,
-  checked: PropTypes.bool.isRequired,
-};
-export default DeleteAllButton;

+ 62 - 0
packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx

@@ -0,0 +1,62 @@
+import React, { FC } from 'react';
+import { useTranslation } from 'react-i18next';
+import loggerFactory from '~/utils/logger';
+import { CheckboxType } from '../../interfaces/search';
+
+const logger = loggerFactory('growi:searchResultList');
+
+type Props = {
+  checkboxState: CheckboxType,
+  onClickInvoked?: () => void,
+  onCheckInvoked?: (string:CheckboxType) => void,
+}
+
+const DeleteSelectedPageGroup:FC<Props> = (props:Props) => {
+  const { t } = useTranslation();
+  const {
+    checkboxState, onClickInvoked, onCheckInvoked,
+  } = props;
+
+  const changeCheckboxStateHandler = () => {
+    console.log(`changeCheckboxStateHandler is called. current changebox state is ${checkboxState}`);
+    // Todo: determine next checkboxState from one of the following and tell the parent component
+    // to change the checkboxState by passing onCheckInvoked function the next checkboxState
+    // - NONE_CHECKED
+    // - INDETERMINATE
+    // - ALL_CHECKED
+    // https://estoc.weseek.co.jp/redmine/issues/77525
+    // use CheckboxType by importing from packages/app/src/interfaces/
+    if (onCheckInvoked == null) { logger.error('onCheckInvoked is null') }
+    else { onCheckInvoked(CheckboxType.ALL_CHECKED) } // change this to an appropriate value
+  };
+
+
+  return (
+    <>
+      <input
+        id="check-all-pages"
+        type="checkbox"
+        name="check-all-pages"
+        className="custom-control custom-checkbox"
+        onChange={changeCheckboxStateHandler}
+        checked={checkboxState === CheckboxType.INDETERMINATE || checkboxState === CheckboxType.ALL_CHECKED}
+      />
+      <button
+        type="button"
+        className="text-danger font-weight-light"
+        onClick={() => {
+          if (onClickInvoked == null) { logger.error('onClickInvoked is null') }
+          else { onClickInvoked() }
+        }}
+      >
+        <i className="icon-trash ml-3"></i>
+        {t('search_result.delete_all_selected_page')}
+      </button>
+    </>
+  );
+
+};
+
+DeleteSelectedPageGroup.propTypes = {
+};
+export default DeleteSelectedPageGroup;

+ 23 - 0
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -2,6 +2,8 @@ import React, { FC } from 'react';
 import { useTranslation } from 'react-i18next';
 import SearchPageForm from './SearchPageForm';
 import AppContainer from '../../client/services/AppContainer';
+import DeleteSelectedPageGroup from './DeleteSelectedPageGroup';
+import { CheckboxType } from '../../interfaces/search';
 
 type Props = {
   searchingKeyword: string,
@@ -29,6 +31,21 @@ const SearchControl: FC <Props> = (props: Props) => {
     }
   };
 
+  const onDeleteSelectedPageHandler = () => {
+    console.log('onDeleteSelectedPageHandler is called');
+    // TODO: implement this function to delete selected pages.
+    // https://estoc.weseek.co.jp/redmine/issues/77525
+  };
+
+  const onCheckAllPagesInvoked = (nextCheckboxState:CheckboxType) => {
+    console.log(`onCheckAllPagesInvoked is called with arg ${nextCheckboxState}`);
+    // Todo: set the checkboxState, isChecked, and indeterminate value of checkbox element according to the passed argument
+    // https://estoc.weseek.co.jp/redmine/issues/77525
+
+    // setting checkbox to indeterminate is required to use of useRef to access checkbox element.
+    // ref: https://getbootstrap.com/docs/4.5/components/forms/#checkboxes
+  };
+
   return (
     <div className="">
       <div className="search-page-input sps sps--abv">
@@ -40,6 +57,12 @@ const SearchControl: FC <Props> = (props: Props) => {
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       <div className="d-flex my-4">
+        {/* Todo: design will be fixed in #80324. Function will be implemented in #77525 */}
+        <DeleteSelectedPageGroup
+          checkboxState={'' || CheckboxType.NONE_CHECKED} // Todo: change the left value to appropriate value
+          onClickInvoked={onDeleteSelectedPageHandler}
+          onCheckInvoked={onCheckAllPagesInvoked}
+        />
         <div className="d-flex align-items-center border rounded border-gray px-2 py-1 mr-2 ml-auto">
           <label className="my-0 mr-2" htmlFor="flexCheckDefault">
             {t('Include Subordinated Target Page', { target: '/user' })}

+ 31 - 12
packages/app/src/components/SearchPage/SearchResultList.jsx

@@ -1,22 +1,37 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import SearchResultListItem from './SearchResultListItem';
+import PaginationWrapper from '../PaginationWrapper';
 
 class SearchResultList extends React.Component {
 
   render() {
-    return this.props.pages.map((page) => {
-      // 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.
-      // TASK : https://estoc.weseek.co.jp/redmine/issues/79606
-      return (
-        <SearchResultListItem
-          page={page}
-          onClickInvoked={this.props.onClickInvoked}
-          noLink
-        />
-      );
-    });
+    return (
+      <>
+        {this.props.pages.map((page) => {
+        // 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.
+        // TASK : https://estoc.weseek.co.jp/redmine/issues/79606
+          return (
+            <SearchResultListItem
+              page={page}
+              onClickInvoked={this.props.onClickInvoked}
+              noLink
+            />
+          );
+        })}
+        {this.props.searchResultCount != null && this.props.searchResultCount > 0 && (
+          <div className="my-4 mx-auto">
+            <PaginationWrapper
+              activePage={this.props.activePage}
+              changePage={this.props.onPagingNumberChanged}
+              totalItemsCount={this.props.searchResultCount || 0}
+              pagingLimit={this.props.pagingLimit}
+            />
+          </div>
+        )}
+      </>
+    );
   }
 
 }
@@ -25,8 +40,12 @@ SearchResultList.propTypes = {
   pages: PropTypes.array.isRequired,
   deletionMode: PropTypes.bool.isRequired,
   selectedPages: PropTypes.array.isRequired,
+  searchResultCount: PropTypes.number,
+  activePage: PropTypes.number.isRequired,
+  pagingLimit: PropTypes.number,
   onClickInvoked: PropTypes.func,
   onChangeInvoked: PropTypes.func,
+  onPagingNumberChanged: PropTypes.func,
 };
 
 export default SearchResultList;

+ 5 - 0
packages/app/src/interfaces/search.ts

@@ -0,0 +1,5 @@
+export enum CheckboxType {
+  NONE_CHECKED = 'noneChecked',
+  INDETERMINATE = 'indeterminate',
+  ALL_CHECKED = 'allChecked',
+}