Explorar o código

Merge pull request #4427 from weseek/feat/77515-77833-arrange-component-temporaly

feat: move search form and add dummy data
Yuki Takei %!s(int64=4) %!d(string=hai) anos
pai
achega
fcfc97cdca

+ 2 - 1
packages/app/src/components/SearchForm.jsx

@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 
@@ -174,4 +175,4 @@ SearchForm.defaultProps = {
   onInputChange: () => {},
 };
 
-export default SearchFormWrapper;
+export default withTranslation()(SearchFormWrapper);

+ 87 - 24
packages/app/src/components/SearchPage.jsx

@@ -8,24 +8,31 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 
 import { toastError } from '~/client/util/apiNotification';
-
-import SearchPageForm from './SearchPage/SearchPageForm';
-import SearchResult from './SearchPage/SearchResult';
+import SearchPageLayout from './SearchPage/SearchPageLayout';
+import SearchResultContent from './SearchPage/SearchResultContent';
+import SearchResultList from './SearchPage/SearchResultList';
+import SearchControl from './SearchPage/SearchControl';
 
 class SearchPage extends React.Component {
 
   constructor(props) {
     super(props);
-
+    // NOTE : selectedPages is deletion related state, will be used later in story 77535, 77565.
+    // deletionModal, deletion related functions are all removed, add them back when necessary.
+    // i.e ) in story 77525 or any tasks implementing deletion functionalities
     this.state = {
       searchingKeyword: decodeURI(this.props.query.q) || '',
       searchedKeyword: '',
       searchedPages: [],
       searchResultMeta: {},
+      selectedPage: {},
+      selectedPages: new Set(),
     };
 
-    this.search = this.search.bind(this);
     this.changeURL = this.changeURL.bind(this);
+    this.search = this.search.bind(this);
+    this.selectPage = this.selectPage.bind(this);
+    this.toggleCheckBox = this.toggleCheckBox.bind(this);
   }
 
   componentDidMount() {
@@ -58,6 +65,7 @@ class SearchPage extends React.Component {
     }
   }
 
+
   search(data) {
     const keyword = data.keyword;
     if (keyword === '') {
@@ -73,38 +81,93 @@ class SearchPage extends React.Component {
     this.setState({
       searchingKeyword: keyword,
     });
-
     this.props.appContainer.apiGet('/search', { q: keyword })
       .then((res) => {
         this.changeURL(keyword);
-
-        this.setState({
-          searchedKeyword: keyword,
-          searchedPages: res.data,
-          searchResultMeta: res.meta,
-        });
+        if (res.data.length > 0) {
+          // TODO: remove creating dummy snippet lines when the data with snippet is abole to be retrieved
+          res.data.forEach((page) => {
+            page.snippet = `dummy snippet dummpy snippet dummpy snippet dummpy snippet dummpy snippet
+            dummpy snippet dummpy snippet dummpy snippet dummpy snippet`;
+          });
+          this.setState({
+            searchedKeyword: keyword,
+            searchedPages: res.data,
+            searchResultMeta: res.meta,
+            selectedPage: res.data[0],
+          });
+        }
       })
       .catch((err) => {
         toastError(err);
       });
   }
 
+  selectPage= (pageId) => {
+    const index = this.state.searchedPages.findIndex((page) => {
+      return page._id === pageId;
+    });
+    this.setState({
+      selectedPage: this.state.searchedPages[index],
+    });
+  }
+
+  toggleCheckBox = (page) => {
+    if (this.state.selectedPages.has(page)) {
+      this.state.selectedPages.delete(page);
+    }
+    else {
+      this.state.selectedPages.add(page);
+    }
+  }
+
+  renderSearchResultContent = () => {
+    return (
+      <SearchResultContent
+        appContainer={this.props.appContainer}
+        searchingKeyword={this.state.searchingKeyword}
+        selectedPage={this.state.selectedPage}
+      >
+      </SearchResultContent>
+    );
+  }
+
+  renderSearchResultList = () => {
+    return (
+      <SearchResultList
+        pages={this.state.searchedPages}
+        deletionMode={false}
+        selectedPage={this.state.selectedPage}
+        selectedPages={this.state.selectedPages}
+        onClickInvoked={this.selectPage}
+        onChangedInvoked={this.toggleCheckBox}
+      >
+      </SearchResultList>
+    );
+  }
+
+  renderSearchControl = () => {
+    return (
+      <SearchControl
+        searchingKeyword={this.state.searchingKeyword}
+        appContainer={this.props.appContainer}
+        onSearchInvoked={this.search}
+      >
+      </SearchControl>
+    );
+  }
+
   render() {
     return (
       <div>
-        {/* 2021/9/22 TODO: Move to SearchResult */}
-        {/* <div className="search-page-input sps sps--abv">
-          <SearchPageForm
-            t={this.props.t}
-            onSearchFormChanged={this.search}
-            keyword={this.state.searchingKeyword}
-          />
-        </div> */}
-        <SearchResult
-          pages={this.state.searchedPages}
-          searchingKeyword={this.state.searchingKeyword}
+        <SearchPageLayout
+          SearchControl={this.renderSearchControl}
+          SearchResultList={this.renderSearchResultList}
+          SearchResultContent={this.renderSearchResultContent}
           searchResultMeta={this.state.searchResultMeta}
-        />
+          searchingKeyword={this.state.searchedKeyword}
+        >
+        </SearchPageLayout>
       </div>
     );
   }

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

@@ -0,0 +1,31 @@
+import React, { FC } from 'react';
+import SearchPageForm from './SearchPageForm';
+import AppContainer from '../../client/services/AppContainer';
+
+
+type Props = {
+  searchingKeyword: string,
+  appContainer: AppContainer,
+  onSearchInvoked: (data : any[]) => boolean,
+}
+
+const SearchControl: FC <Props> = (props: Props) => {
+  // Temporaly workaround for lint error
+  // later needs to be fixed: SearchControl to typescript componet
+  const SearchPageFormTypeAny : any = SearchPageForm;
+  return (
+    <div className="">
+      <div className="search-page-input sps sps--abv">
+        <SearchPageFormTypeAny
+          keyword={props.searchingKeyword}
+          appContainer={props.appContainer}
+          onSearchFormChanged={props.onSearchInvoked}
+        />
+      </div>
+      {/* TODO: place deleteAll button , relevance button , include specificPath button */}
+    </div>
+  );
+};
+
+
+export default SearchControl;

+ 25 - 7
packages/app/src/components/SearchPage/SearchPageForm.jsx

@@ -4,6 +4,9 @@ import PropTypes from 'prop-types';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import SearchForm from '../SearchForm';
+import loggerFactory from '~/utils/logger';
+
+const logger = loggerFactory('growi:searchPageForm');
 
 // Search.SearchForm
 class SearchPageForm extends React.Component {
@@ -21,9 +24,14 @@ class SearchPageForm extends React.Component {
   }
 
   search() {
-    const keyword = this.state.keyword;
-    this.props.onSearchFormChanged({ keyword });
-    this.setState({ searchedKeyword: keyword });
+    if (this.props.onSearchFormChanged != null) {
+      const keyword = this.state.keyword;
+      this.props.onSearchFormChanged({ keyword });
+      this.setState({ searchedKeyword: keyword });
+    }
+    else {
+      throw new Error('onSearchFormChanged method is null');
+    }
   }
 
   onInputChange(input) { // for only submitting with button
@@ -35,14 +43,25 @@ class SearchPageForm extends React.Component {
       <div className="input-group mb-3 d-flex">
         <div className="flex-fill">
           <SearchForm
-            t={this.props.t}
             onSubmit={this.search}
             keyword={this.state.searchedKeyword}
             onInputChange={this.onInputChange}
           />
         </div>
         <div className="input-group-append">
-          <button className="btn btn-secondary" type="button" id="button-addon2" onClick={this.search}>
+          <button
+            className="btn btn-secondary"
+            type="button"
+            id="button-addon2"
+            onClick={() => {
+              try {
+                this.search();
+              }
+              catch (error) {
+                logger.error(error);
+              }
+            }}
+          >
             <i className="icon-magnifier"></i>
           </button>
         </div>
@@ -58,11 +77,10 @@ class SearchPageForm extends React.Component {
 const SearchPageFormWrapper = withUnstatedContainers(SearchPageForm, [AppContainer]);
 
 SearchPageForm.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
   keyword: PropTypes.string,
-  onSearchFormChanged: PropTypes.func.isRequired,
+  onSearchFormChanged: PropTypes.func,
 };
 SearchPageForm.defaultProps = {
 };

+ 43 - 0
packages/app/src/components/SearchPage/SearchPageLayout.tsx

@@ -0,0 +1,43 @@
+import React, { FC } from 'react';
+
+type SearchResultMeta = {
+  took : number,
+  total : number,
+  results: number
+}
+
+type Props = {
+  SearchControl: React.FunctionComponent,
+  SearchResultList: React.FunctionComponent,
+  SearchResultContent: React.FunctionComponent,
+  searchResultMeta: SearchResultMeta,
+  searchingKeyword: string
+}
+
+const SearchPageLayout: FC<Props> = (props: Props) => {
+  const { SearchResultList, SearchControl, SearchResultContent } = 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">
+          <nav><SearchControl></SearchControl></nav>
+          <div className="d-flex align-items-start justify-content-between mt-1">
+            <div className="search-result-meta">
+              <i className="icon-magnifier" /> Found {props.searchResultMeta.total} pages with &quot;{props.searchingKeyword}&quot;
+            </div>
+          </div>
+
+          <div className="page-list">
+            <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">
+          <SearchResultContent></SearchResultContent>
+        </div>
+      </div>
+    </div>
+  );
+};
+
+
+export default SearchPageLayout;

+ 2 - 0
packages/app/src/components/SearchPage/SearchResult.jsx

@@ -10,6 +10,8 @@ import DeletePageListModal from './DeletePageListModal';
 import AppContainer from '~/client/services/AppContainer';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
+// NOTE : this file will be deleted in the future. Merge conflict happend in this file, so temporaly kept this left here.
+// Task 77833 deleted this file ;
 class SearchResult extends React.Component {
 
   constructor(props) {

+ 50 - 0
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -0,0 +1,50 @@
+import React, { FC } from 'react';
+
+import RevisionLoader from '../Page/RevisionLoader';
+import AppContainer from '../../client/services/AppContainer';
+
+
+type Props ={
+  appContainer: AppContainer,
+  searchingKeyword:string,
+  selectedPage : any,
+}
+const SearchResultContent: FC<Props> = (props: Props) => {
+  // Temporaly workaround for lint error
+  // later needs to be fixed: RevisoinRender to typescriptcomponet
+  const RevisionRenderTypeAny: any = RevisionLoader;
+  const renderPage = (page) => {
+    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">
+        <h2>
+          <a href={page.path} className="text-break">
+            {page.path}
+          </a>
+          {showTags && (
+            <div className="mt-1 small">
+              <i className="tag-icon icon-tag"></i> {page.tags.join(', ')}
+            </div>
+          )}
+        </h2>
+        <RevisionRenderTypeAny
+          growiRenderer={growiRenderer}
+          pageId={page._id}
+          pagePath={page.path}
+          revisionId={page.revision}
+          highlightKeywords={props.searchingKeyword}
+        />
+      </div>
+    );
+  };
+  const content = renderPage(props.selectedPage);
+  return (
+
+    <div>{content}</div>
+  );
+};
+
+
+export default SearchResultContent;

+ 70 - 45
packages/app/src/components/SearchPage/SearchResultList.jsx

@@ -1,64 +1,89 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import Page from '../PageList/Page';
+import loggerFactory from '~/utils/logger';
 
-import RevisionLoader from '../Page/RevisionLoader';
-import AppContainer from '~/client/services/AppContainer';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
+const logger = loggerFactory('growi:searchResultList');
 class SearchResultList extends React.Component {
 
-  constructor(props) {
-    super(props);
-
-    this.growiRenderer = this.props.appContainer.getRenderer('searchresult');
-  }
-
   render() {
-    const resultList = this.props.pages.map((page) => {
-      const showTags = (page.tags != null) && (page.tags.length > 0);
-
+    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 (
-        // Add prefix 'id_' in id attr, because scrollspy of bootstrap doesn't work when the first letter of id of target component is numeral.
-        <div id={`id_${page._id}`} key={page._id} className="search-result-page mb-5">
-          <h2>
-            <a href={page.path} className="text-break">{page.path}</a>
-            { showTags && (
-              <div className="mt-1 small"><i className="tag-icon icon-tag"></i> {page.tags.join(', ')}</div>
-            )}
-          </h2>
-          <RevisionLoader
-            growiRenderer={this.growiRenderer}
-            pageId={page._id}
-            pagePath={page.path}
-            revisionId={page.revision}
-            highlightKeywords={this.props.searchingKeyword}
-          />
-        </div>
+        <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>
       );
     });
-
-    return (
-      <div>
-        {resultList}
-      </div>
-    );
   }
 
 }
 
-/**
- * Wrapper component for using unstated
- */
-const SearchResultListWrapper = withUnstatedContainers(SearchResultList, [AppContainer]);
 
 SearchResultList.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   pages: PropTypes.array.isRequired,
-  searchingKeyword: PropTypes.string.isRequired,
+  deletionMode: PropTypes.bool.isRequired,
+  selectedPages: PropTypes.array.isRequired,
+  onClickInvoked: PropTypes.func,
+  onChangeInvoked: PropTypes.func,
 };
 
-SearchResultList.defaultProps = {
-};
 
-export default SearchResultListWrapper;
+export default SearchResultList;