Преглед изворни кода

Merge branch 'master' into imprv/growi_renderer-in-reveal

Yuto Iwata пре 7 година
родитељ
комит
959f677b09

+ 18 - 1
CHANGES.md

@@ -1,12 +1,29 @@
 CHANGES
 ========
 
-## 3.3.7-RC
+## 3.3.9
+
+* 
+
+## 3.3.8
+
+* Fix: Typeahead shows autocomplete wrongly
+* Fix: Move/Duplicate don't work
+    * Introduced by 3.3.7
+* Fix: Server doesn't respond when root page is restricted
+* Support: Upgrade libs
+    * react
+    * react-bootstrap-typeahead
+
+## 3.3.7
 
 * Feature: Editor toolbar
+* Feature: `prefix:/path` searching syntax to filter with page path prefix
+* Feature: Add an option to filter only children to searching box of navbar
 * Improvement: Suggest page path when moving/duplicating/searching
 * Fix: Anonymous users couldn't search
     * Introduced by 3.3.6
+* I18n: Searching help
 * Support: Prepare to suppoert Node.js v10
 * Support: Upgrade libs
     * node-sass

+ 3 - 3
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.3.7-RC",
+  "version": "3.3.9-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -182,9 +182,9 @@
     "penpal": "^3.0.3",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
-    "react": "^16.4.1",
+    "react": "^16.7.0",
     "react-bootstrap": "^0.32.1",
-    "react-bootstrap-typeahead": "^3.1.5",
+    "react-bootstrap-typeahead": "^3.3.2",
     "react-clipboard.js": "^2.0.0",
     "react-codemirror2": "^5.1.0",
     "react-dom": "^16.4.1",

+ 32 - 0
resource/locales/en-US/translation.json

@@ -163,6 +163,38 @@
 
   "Security settings": "Security settings",
 
+  "header_search_box": {
+    "label": {
+      "This tree": "This tree"
+    },
+    "item_label": {
+      "This tree": "Only children of this tree"
+    }
+  },
+
+  "search_help": {
+    "title": "Searching Help",
+    "and": {
+      "syntax help": "divide with space",
+      "desc": "Search pages that include both {{word1}}, {{word2}} in the title or body"
+    },
+    "exclude": {
+      "desc": "Exclude pages that include {{word}} in the title or body"
+    },
+    "phrase": {
+      "syntax help": "surround with double quotes",
+      "desc": "Search pages that include the phrase \"{{phrase}}\""
+    },
+    "prefix": {
+      "desc": "Search only the pages that the title start with {{path}}"
+    },
+    "exclude_prefix": {
+      "desc": "Exclude the pages that the title start with {{path}}"
+    }
+  },
+  "search": {
+    "search page bodies": "Hit [Enter] key to full-text search"
+  },
 
   "page_page": {
       "notice": {

+ 33 - 0
resource/locales/ja/translation.json

@@ -181,6 +181,39 @@
   "Current API Token": "現在のAPI Token",
   "Update API Token": "API Tokenを更新",
 
+  "header_search_box": {
+    "label": {
+      "This tree": "この階層"
+    },
+    "item_label": {
+      "This tree": "この階層下の子ページのみ"
+    }
+  },
+
+  "search_help": {
+    "title": "検索のヘルプ",
+    "and": {
+      "syntax help": "スペース区切り",
+      "desc": "ページ名 or 本文に {{word1}}, {{word2}} の両方を含むページを検索"
+    },
+    "exclude": {
+      "desc": "ページ名 or 本文に {{word}} を含むページを除外"
+    },
+    "phrase": {
+      "syntax help": "ダブルクォートで囲う",
+      "desc": "{{phrase}} という文章を含むページを検索"
+    },
+    "prefix": {
+      "desc": "ページ名が {{path}} から始まるページに絞る"
+    },
+    "exclude_prefix": {
+      "desc": "ページ名が {{path}} から始まるページを除外"
+    }
+  },
+  "search": {
+    "search page bodies": "[Enter] キー押下で全文検索"
+  },
+
   "page_page": {
       "notice": {
           "version": "これは現在の版ではありません。",

+ 6 - 8
src/client/js/app.js

@@ -20,7 +20,6 @@ import { EditorOptions, PreviewOptions } from './components/PageEditor/OptionsSe
 import SavePageControls from './components/SavePageControls';
 import PageEditorByHackmd from './components/PageEditorByHackmd';
 import Page             from './components/Page';
-import PageListSearch   from './components/PageListSearch';
 import PageHistory      from './components/PageHistory';
 import PageComments     from './components/PageComments';
 import CommentForm from './components/PageComment/CommentForm';
@@ -30,7 +29,7 @@ import SeenUserList     from './components/SeenUserList';
 import RevisionPath     from './components/Page/RevisionPath';
 import RevisionUrl      from './components/Page/RevisionUrl';
 import BookmarkButton   from './components/BookmarkButton';
-import NewPageNameInput from './components/NewPageNameInput';
+import PagePathAutoComplete from './components/PagePathAutoComplete';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 
 import CustomCssEditor  from './components/Admin/CustomCssEditor';
@@ -273,19 +272,18 @@ if (!pageRevisionId && draft != null) {
  *  value: React Element
  */
 const componentMappings = {
-  'search-top': <HeaderSearchBox crowi={crowi} />,
+  'search-top': <I18nextProvider i18n={i18n}><HeaderSearchBox crowi={crowi} /></I18nextProvider>,
   'search-sidebar': <HeaderSearchBox crowi={crowi} />,
-  'search-page': <SearchPage crowi={crowi} crowiRenderer={crowiRenderer} />,
-  'page-list-search': <PageListSearch crowi={crowi} />,
+  'search-page': <I18nextProvider i18n={i18n}><SearchPage crowi={crowi} crowiRenderer={crowiRenderer} /></I18nextProvider>,
 
   //'revision-history': <PageHistory pageId={pageId} />,
   'seen-user-list': <SeenUserList pageId={pageId} crowi={crowi} />,
   'bookmark-button': <BookmarkButton pageId={pageId} crowi={crowi} />,
   'bookmark-button-lg': <BookmarkButton pageId={pageId} crowi={crowi} size="lg" />,
 
-  'create-page-name-input': <NewPageNameInput crowi={crowi} parentPageName={pagePath} />,
-  'rename-page-name-input': <NewPageNameInput crowi={crowi} parentPageName={pagePath} />,
-  'duplicate-page-name-input': <NewPageNameInput crowi={crowi} parentPageName={pagePath} />,
+  'create-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} addSlashToTheEnd={true} />,
+  'rename-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
+  'duplicate-page-name-input': <PagePathAutoComplete crowi={crowi} initializedPath={pagePath} />,
 
 };
 // additional definitions if data exists

+ 0 - 37
src/client/js/components/HeaderSearchBox.js

@@ -1,37 +0,0 @@
-// This is the root component for #search-top
-
-import React from 'react';
-
-import HeaderSearchForm from './HeaderSearchBox/HeaderSearchForm';
-// import SearchSuggest from './HeaderSearchBox/SearchSuggest'; // omit since using react-bootstrap-typeahead in SearchForm
-
-export default class SearchBox extends React.Component {
-
-  constructor(props) {
-    super(props);
-  }
-
-  render() {
-    return (
-      <div className="search-box">
-        <HeaderSearchForm />
-        {/* omit since using react-bootstrap-typeahead in SearchForm
-        <SearchSuggest
-          searchingKeyword={this.state.searchingKeyword}
-          searchedPages={this.state.searchedPages}
-          searchError={this.state.searchError}
-          searching={this.state.searching}
-          focused={this.state.focused}
-          />
-        */}
-      </div>
-    );
-  }
-}
-
-SearchBox.propTypes = {
-  //pollInterval: PropTypes.number,
-};
-SearchBox.defaultProps = {
-  //pollInterval: 1000,
-};

+ 99 - 0
src/client/js/components/HeaderSearchBox.jsx

@@ -0,0 +1,99 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { translate } from 'react-i18next';
+
+import FormGroup from 'react-bootstrap/es/FormGroup';
+import Button from 'react-bootstrap/es/Button';
+import DropdownButton from 'react-bootstrap/es/DropdownButton';
+import MenuItem from 'react-bootstrap/es/MenuItem';
+import InputGroup from 'react-bootstrap/es/InputGroup';
+
+import SearchForm from './SearchForm';
+
+
+class HeaderSearchBox extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      text: '',
+      isScopeChildren: false,
+    };
+
+    this.onInputChange = this.onInputChange.bind(this);
+    this.onClickAllPages = this.onClickAllPages.bind(this);
+    this.onClickChildren = this.onClickChildren.bind(this);
+    this.search = this.search.bind(this);
+  }
+
+  componentDidMount() {
+  }
+
+  componentWillUnmount() {
+  }
+
+  onInputChange(text) {
+    this.setState({ text });
+  }
+
+  onClickAllPages() {
+    this.setState({ isScopeChildren: false });
+  }
+
+  onClickChildren() {
+    this.setState({ isScopeChildren: true });
+  }
+
+  search() {
+    const url = new URL(location.href);
+    url.pathname = '/_search';
+
+    // construct search query
+    let q = this.state.text;
+    if (this.state.isScopeChildren) {
+      q += ` prefix:${location.pathname}`;
+    }
+    url.searchParams.append('q', q);
+
+    location.href = url.href;
+  }
+
+  render() {
+    const t = this.props.t;
+    const scopeLabel = this.state.isScopeChildren
+      ? t('header_search_box.label.This tree')
+      : 'All pages';
+
+    return (
+      <FormGroup>
+        <InputGroup>
+        <InputGroup.Button className="btn-group-dropdown-scope">
+          <DropdownButton id="dbScope" title={scopeLabel}>
+            <MenuItem onClick={this.onClickAllPages}>All pages</MenuItem>
+            <MenuItem onClick={this.onClickChildren}>{ t('header_search_box.item_label.This tree') }</MenuItem>
+          </DropdownButton>
+        </InputGroup.Button>
+          <SearchForm t={this.props.t}
+            crowi={this.props.crowi}
+            onInputChange={this.onInputChange}
+            onSubmit={this.search}
+            placeholder="Search ..."
+          />
+          <InputGroup.Button className="btn-group-submit-search">
+            <Button bsStyle="link" onClick={this.search}>
+              <i className="icon-magnifier"></i>
+            </Button >
+          </InputGroup.Button>
+        </InputGroup>
+      </FormGroup>
+    );
+  }
+}
+
+HeaderSearchBox.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
+  crowi: PropTypes.object.isRequired,
+};
+
+export default translate()(HeaderSearchBox);

+ 0 - 63
src/client/js/components/HeaderSearchBox/HeaderSearchForm.js

@@ -1,63 +0,0 @@
-import React from 'react';
-
-import FormGroup from 'react-bootstrap/es/FormGroup';
-import Button from 'react-bootstrap/es/Button';
-import InputGroup from 'react-bootstrap/es/InputGroup';
-
-import SearchForm from '../SearchForm';
-
-
-// Header.SearchForm
-export default class HeaderSearchForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.crowi = window.crowi; // FIXME
-
-    this.onSubmit = this.onSubmit.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
-  }
-
-  onSubmit() {
-    this.refs.form.submit();
-  }
-
-  render() {
-    return (
-      <form
-        ref='form'
-        action='/_search'
-        className='search-form form-group input-group search-input-group hidden-print'
-      >
-        <FormGroup>
-          <InputGroup>
-            <SearchForm
-              crowi={this.crowi}
-              onSubmit={this.onSubmit}
-              placeholder="Search ..."
-            />
-            <InputGroup.Button>
-              <Button type="submit" bsStyle="link">
-                <i className="icon-magnifier"></i>
-              </Button >
-            </InputGroup.Button>
-          </InputGroup>
-        </FormGroup>
-
-      </form>
-
-    );
-  }
-}
-
-HeaderSearchForm.propTypes = {
-};
-
-HeaderSearchForm.defaultProps = {
-};

+ 0 - 61
src/client/js/components/HeaderSearchBox/SearchSuggest.js

@@ -1,61 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import ListView from '../PageList/ListView';
-
-export default class SearchSuggest extends React.Component {
-
-  render() {
-    if (!this.props.focused) {
-      return <div></div>;
-    }
-
-    if (this.props.searching) {
-      return (
-        <div className="search-suggest" id="search-suggest">
-          <i className="searcing fa fa-circle-o-notch fa-spin fa-fw"></i> Searching ...
-        </div>
-      );
-    }
-
-    if (this.props.searchError !== null) {
-      return (
-        <div className="search-suggest" id="search-suggest">
-          <i className="searcing fa fa-warning"></i> Error on searching.
-        </div>
-      );
-    }
-
-    if (this.props.searchedPages.length < 1) {
-      if (this.props.searchingKeyword !== '') {
-        return (
-          <div className="search-suggest" id="search-suggest">
-            No results for "{this.props.searchingKeyword}".
-          </div>
-        );
-      }
-      return <div></div>;
-    }
-
-    return (
-      <div className="search-suggest" id="search-suggest">
-        <ListView pages={this.props.searchedPages} />
-      </div>
-    );
-  }
-
-}
-
-SearchSuggest.propTypes = {
-  searchedPages: PropTypes.array.isRequired,
-  searchingKeyword: PropTypes.string.isRequired,
-  searching: PropTypes.bool.isRequired,
-};
-
-SearchSuggest.defaultProps = {
-  searchedPages: [],
-  searchingKeyword: '',
-  searchError: null,
-  searching: false,
-  focused: false,
-};

+ 0 - 82
src/client/js/components/NewPageNameInput.js

@@ -1,82 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import SearchTypeahead from './SearchTypeahead';
-
-export default class NewPageNameInput extends React.Component {
-
-  constructor(props) {
-
-    super(props);
-
-    this.state = {
-      searchError: null,
-    };
-    this.crowi = this.props.crowi;
-
-    this.onSearchError = this.onSearchError.bind(this);
-    this.onSubmit = this.onSubmit.bind(this);
-    this.getParentPageName = this.getParentPageName.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
-  }
-
-  onSearchError(err) {
-    this.setState({
-      searchError: err,
-    });
-  }
-
-  onSubmit(query) {
-    // get the closest form element
-    const elem = this.refs.rootDom;
-    const form = elem.closest('form');
-    // submit with jQuery
-    $(form).submit();
-  }
-
-  getParentPageName(path) {
-    if (path == '/') {
-      return path;
-    }
-
-    if (path.match(/.+\/$/)) {
-      return path;
-    }
-
-    return path + '/';
-  }
-
-  render() {
-    const emptyLabel = (this.state.searchError !== null)
-      ? 'Error on searching.'
-      : 'No matches found on title...';
-
-    return (
-      <div ref='rootDom'>
-        <SearchTypeahead
-          ref={this.searchTypeaheadDom}
-          crowi={this.crowi}
-          onSearchError={this.onSearchError}
-          onSubmit={this.onSubmit}
-          emptyLabel={emptyLabel}
-          placeholder="Input page name"
-          keywordOnInit={this.getParentPageName(this.props.parentPageName)}
-        />
-      </div>
-    );
-  }
-}
-
-NewPageNameInput.propTypes = {
-  crowi:          PropTypes.object.isRequired,
-  parentPageName: PropTypes.string,
-};
-
-NewPageNameInput.defaultProps = {
-  parentPageName: '',
-};

+ 0 - 169
src/client/js/components/PageListSearch.js

@@ -1,169 +0,0 @@
-// This is the root component for #page-list-search
-
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import SearchResult from './SearchPage/SearchResult';
-
-export default class PageListSearch extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      location: location,
-      tree: $('#search-listpage-input').data('path'),
-      searchingKeyword: this.props.query.q || '',
-      searchedKeyword: '',
-      searchedPages: [],
-      searchResultMeta: {},
-      searchError: null,
-    };
-
-    this.changeURL = this.changeURL.bind(this);
-    this.search = this.search.bind(this);
-    this.handleChange = this.handleChange.bind(this);
-  }
-
-  componentDidMount() {
-    const $pageListSearchForm = $('#search-listpage-input');
-
-    // search after page initialized
-    if (this.state.searchingKeyword !== '') {
-      const keyword = this.state.searchingKeyword;
-      $pageListSearchForm.val(keyword);
-      this.search({keyword});
-    }
-
-    // This is temporary data bind ... (out of component)
-    $('#search-listpage-form').on('submit', () => {
-      const keyword = $pageListSearchForm.val();
-      if (keyword != this.state.searchingKeyword)  {
-        this.search({keyword});
-      }
-
-      return false;
-    });
-
-    $('#search-listpage-clear').on('click', () => {
-      $pageListSearchForm.val('');
-      this.search({keyword: ''});
-
-      return false;
-    });
-  }
-
-  componentWillUnmount() {
-  }
-
-  static getQueryByLocation(location) {
-    let search = location.search || '';
-    let query = {};
-
-    search.replace(/^\?/, '').split('&').forEach(function(element) {
-      let queryParts = element.split('=');
-      query[queryParts[0]] = decodeURIComponent(queryParts[1]).replace(/\+/g, ' ');
-    });
-
-    return query;
-  }
-
-  handleChange(event) {
-    // this is not fired now because of force-data-bound by jQuery
-    const keyword = event.target.value;
-    this.setState({searchedKeyword: keyword});
-    //console.log('Changed');
-  }
-
-  stopSearching() {
-    $('#content-main').show();
-    $('#search-listpage-clear').hide();
-    $('.main-container').removeClass('aside-hidden');
-  }
-
-  startSearching() {
-    $('#content-main').hide();
-    $('#search-listpage-clear').show();
-    $('.main-container').addClass('aside-hidden');
-  }
-
-  changeURL(keyword, refreshHash) {
-    const tree = this.state.tree;
-    let hash = location.hash || '';
-    // TODO 整理する
-    if (refreshHash || this.state.searchedKeyword !== '') {
-      hash = '';
-    }
-    if (window.history && window.history.pushState) {
-      window.history.pushState('', `Search - ${keyword}`, `${tree}?q=${keyword}${hash}`);
-    }
-  }
-
-  search(data) {
-    const keyword = data.keyword || '';
-    const tree = this.state.tree;
-
-    this.changeURL(keyword);
-    if (keyword === '') {
-      this.stopSearching();
-      this.setState({
-        searchingKeyword: '',
-        searchedPages: [],
-        searchResultMeta: {},
-        searchError: null,
-      });
-
-      return true;
-    }
-
-    this.startSearching();
-    this.setState({
-      searchingKeyword: keyword,
-    });
-
-    this.props.crowi.apiGet('/search', {q: keyword, tree: tree})
-    .then((res) => {
-      this.setState({
-        searchedKeyword: keyword,
-        searchedPages: res.data,
-        searchResultMeta: res.meta,
-      });
-    }).catch(err => {
-      this.setState({
-        searchError: err,
-      });
-    });
-  }
-
-  render() {
-    return (
-      <div>
-        <input
-          type="hidden"
-          name="q"
-          value={this.state.searchingKeyword}
-          onChange={this.handleChange}
-          className="form-control"
-          />
-        <SearchResult
-          tree={this.state.tree}
-          pages={this.state.searchedPages}
-          searchingKeyword={this.state.searchingKeyword}
-          searchResultMeta={this.state.searchResultMeta}
-          searchError={this.state.searchError}
-          crowi={this.props.crowi}
-          />
-      </div>
-    );
-  }
-}
-
-PageListSearch.propTypes = {
-  query: PropTypes.object,
-  crowi: PropTypes.object.isRequired,
-};
-PageListSearch.defaultProps = {
-  //pollInterval: 1000,
-  query: PageListSearch.getQueryByLocation(location || {}),
-};
-

+ 67 - 0
src/client/js/components/PagePathAutoComplete.jsx

@@ -0,0 +1,67 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import * as pagePathUtils from '@commons/util/page-path-utils';
+import SearchTypeahead from './SearchTypeahead';
+
+export default class PagePathAutoComplete extends React.Component {
+
+  constructor(props) {
+
+    super(props);
+
+    this.state = {
+      searchError: null,
+    };
+    this.crowi = this.props.crowi;
+
+    this.onSubmit = this.onSubmit.bind(this);
+    this.getKeywordOnInit = this.getKeywordOnInit.bind(this);
+  }
+
+  componentDidMount() {
+  }
+
+  componentWillUnmount() {
+  }
+
+  onSubmit(query) {
+    // get the closest form element
+    const elem = this.refs.rootDom;
+    const form = elem.closest('form');
+    // submit with jQuery
+    $(form).submit();
+  }
+
+  getKeywordOnInit(path) {
+    return this.props.addSlashToTheEnd
+      ? pagePathUtils.addSlashToTheEnd(path)
+      : pagePathUtils.removeLastSlash(path);
+  }
+
+  render() {
+    return (
+      <div ref='rootDom'>
+        <SearchTypeahead
+          ref={this.searchTypeaheadDom}
+          crowi={this.crowi}
+          onSubmit={this.onSubmit}
+          inputName='new_path'
+          emptyLabelExceptError={null}
+          placeholder="Input page path"
+          keywordOnInit={this.getKeywordOnInit(this.props.initializedPath)}
+        />
+      </div>
+    );
+  }
+}
+
+PagePathAutoComplete.propTypes = {
+  crowi:            PropTypes.object.isRequired,
+  initializedPath:  PropTypes.string,
+  addSlashToTheEnd: PropTypes.bool,
+};
+
+PagePathAutoComplete.defaultProps = {
+  initializedPath: '/',
+};

+ 26 - 8
src/client/js/components/SearchForm.js

@@ -38,23 +38,39 @@ export default class SearchForm extends React.Component {
   }
 
   getHelpElement() {
+    const t = this.props.t;
+
     return (
       <table className="table m-1 search-help">
         <caption className="text-left text-primary p-2 mb-2">
-          <h5 className="m-1"><i className="icon-magnifier pr-2 mb-2"/>Search Help</h5>
+          <h5 className="m-1"><i className="icon-magnifier pr-2 mb-2"/>{ t('search_help.title') }</h5>
         </caption>
         <tbody>
           <tr>
-            <td className="text-right mt-0 pr-2 p-1"><code>keyword</code></td>
-            <th className="mr-2"><h6 className="pr-2 m-0 pt-1">記事名 or 本文に<samp>"keyword"</samp>を含む</h6></th>
+            <th className="text-right pt-2">
+              <code>word1</code> <code>word2</code><br></br>
+              <small>({ t('search_help.and.syntax help') })</small>
+            </th>
+            <td><h6 className="m-0 pt-1">{ t('search_help.and.desc', { word1: 'word1', word2: 'word2' }) }</h6></td>
+          </tr>
+          <tr>
+            <th className="text-right pt-2">
+              <code>"This is GROWI"</code><br></br>
+              <small>({ t('search_help.phrase.syntax help') })</small>
+            </th>
+            <td><h6 className="m-0 pt-1">{ t('search_help.phrase.desc', { phrase: 'This is GROWI' }) }</h6></td>
+          </tr>
+          <tr>
+            <th className="text-right pt-2"><code>-keyword</code></th>
+            <td><h6 className="m-0 pt-1">{ t('search_help.exclude.desc', { word: 'keyword' }) }</h6></td>
           </tr>
           <tr>
-            <td className="text-right mt-0 pr-2 p-1"><code>a b</code></td>
-            <th><h6 className="m-0 pt-1">文字列<samp>"a"</samp>と<samp>"b"</samp>を含む (スペース区切り)</h6></th>
+            <th className="text-right pt-2"><code>prefix:/user/</code></th>
+            <td><h6 className="m-0 pt-1">{ t('search_help.prefix.desc', { path: '/user/' }) }</h6></td>
           </tr>
           <tr>
-            <td className="text-right mt-0 pr-2 p-1"><code>-keyword</code></td>
-            <th><h6 className="m-0 pt-1">文字列<samp>"keyword"</samp>を含まない</h6></th>
+            <th className="text-right pt-2"><code>-prefix:/user/</code></th>
+            <td><h6 className="m-0 pt-1">{ t('search_help.exclude_prefix.desc', { path: '/user/' }) }</h6></td>
           </tr>
         </tbody>
       </table>
@@ -62,9 +78,10 @@ export default class SearchForm extends React.Component {
   }
 
   render() {
+    const t = this.props.t;
     const emptyLabel = (this.state.searchError !== null)
       ? 'Error on searching.'
-      : 'No matches found on title... Hit [Enter] key so that search on contents.';
+      : t('search.search page bodies');
 
     return (
       <SearchTypeahead
@@ -83,6 +100,7 @@ export default class SearchForm extends React.Component {
 }
 
 SearchForm.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
   crowi: PropTypes.object.isRequired,
   keyword: PropTypes.string,
   onSubmit: PropTypes.func.isRequired,

+ 5 - 2
src/client/js/components/SearchPage.js

@@ -2,11 +2,12 @@
 
 import React from 'react';
 import PropTypes from 'prop-types';
+import { translate } from 'react-i18next';
 
 import SearchPageForm from './SearchPage/SearchPageForm';
 import SearchResult from './SearchPage/SearchResult';
 
-export default class SearchPage extends React.Component {
+class SearchPage extends React.Component {
 
   constructor(props) {
     super(props);
@@ -92,7 +93,7 @@ export default class SearchPage extends React.Component {
     return (
       <div>
         <div className="search-page-input">
-          <SearchPageForm
+          <SearchPageForm t={this.props.t}
             crowi={this.props.crowi}
             onSearchFormChanged={this.search}
             keyword={this.state.searchingKeyword}
@@ -110,6 +111,7 @@ export default class SearchPage extends React.Component {
 }
 
 SearchPage.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
   crowi: PropTypes.object.isRequired,
   crowiRenderer: PropTypes.object.isRequired,
   query: PropTypes.object,
@@ -120,3 +122,4 @@ SearchPage.defaultProps = {
   searchError: null,
 };
 
+export default translate()(SearchPage);

+ 20 - 24
src/client/js/components/SearchPage/SearchPageForm.js

@@ -1,5 +1,10 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+
+import FormGroup from 'react-bootstrap/es/FormGroup';
+import Button from 'react-bootstrap/es/Button';
+import InputGroup from 'react-bootstrap/es/InputGroup';
+
 import SearchForm from '../SearchForm';
 
 // Search.SearchForm
@@ -14,20 +19,13 @@ export default class SearchPageForm extends React.Component {
     };
 
     this.search = this.search.bind(this);
-    this.onSubmit = this.onSubmit.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
   }
 
-  search(keyword) {
-    if (this.state.searchedKeyword != keyword) {
-      this.props.onSearchFormChanged({keyword: keyword});
-      this.setState({searchedKeyword: keyword});
-    }
-  }
-
-  onSubmit(event) { // submit with button
-    event.preventDefault(); // prevent refreshing page
-    this.search(this.state.keyword);
+  search() {
+    const keyword = this.state.keyword;
+    this.props.onSearchFormChanged({keyword: keyword});
+    this.setState({searchedKeyword: keyword});
   }
 
   onInputChange(input) { // for only submitting with button
@@ -35,28 +33,26 @@ export default class SearchPageForm extends React.Component {
   }
 
   render() {
-    return (
-      <form ref='form'
-        className="form form-group input-group"
-        onSubmit={this.onSubmit}
-      >
-        <SearchForm
+    return <FormGroup>
+      <InputGroup>
+        <SearchForm t={this.props.t}
           crowi={this.props.crowi}
           onSubmit={this.search}
           keyword={this.state.searchedKeyword}
           onInputChange={this.onInputChange}
         />
-        <span className="input-group-btn">
-          <button type="submit" className="btn btn-default">
-            <i className="search-top-icon icon-magnifier"></i>
-          </button>
-        </span>
-      </form>
-    );
+        <InputGroup.Button className="">
+          <Button onClick={this.search}>
+            <i className="icon-magnifier"></i>
+          </Button >
+        </InputGroup.Button>
+      </InputGroup>
+    </FormGroup>;
   }
 }
 
 SearchPageForm.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
   crowi: PropTypes.object.isRequired,
   keyword: PropTypes.string,
   onSearchFormChanged: PropTypes.func.isRequired,

+ 51 - 25
src/client/js/components/SearchTypeahead.js

@@ -23,15 +23,15 @@ export default class SearchTypeahead extends React.Component {
       searchError: null,
     };
     this.crowi = this.props.crowi;
-    this.emptyLabel = props.emptyLabel;
 
+    this.restoreInitialData = this.restoreInitialData.bind(this);
     this.search = this.search.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
     this.dispatchSubmit = this.dispatchSubmit.bind(this);
+    this.getEmptyLabel = this.getEmptyLabel.bind(this);
     this.getRestoreFormButton = this.getRestoreFormButton.bind(this);
     this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
-    this.restoreInitialData = this.restoreInitialData.bind(this);
     this.getTypeahead = this.getTypeahead.bind(this);
   }
 
@@ -48,6 +48,17 @@ export default class SearchTypeahead extends React.Component {
   componentWillUnmount() {
   }
 
+  /**
+   * Initialize keyword
+   */
+  restoreInitialData() {
+    // see https://github.com/ericgio/react-bootstrap-typeahead/issues/266#issuecomment-414987723
+    const text = this.props.keywordOnInit;
+    const instance = this.refs.typeahead.getInstance();
+    instance.clear();
+    instance.setState({ text });
+  }
+
   search(keyword) {
 
     if (keyword === '') {
@@ -110,23 +121,20 @@ export default class SearchTypeahead extends React.Component {
     }
   }
 
-  renderMenuItemChildren(option, props, index) {
-    const page = option;
-    return (
-      <span>
-      <UserPicture user={page.lastUpdateUser} size="sm" />
-      <PagePath page={page} />
-      <PageListMeta page={page} />
-      </span>
-    );
-  }
+  getEmptyLabel() {
+    // use props.emptyLabel as is if defined
+    if (this.props.emptyLabel !== undefined) {
+      return this.props.emptyLabel;
+    }
 
-  /**
-   * Initialize keyword
-   */
-  restoreInitialData() {
-    this.refs.typeahead.getInstance().clear();
-    this.refs.typeahead.getInstance()._updateText(this.props.keywordOnInit);
+    let emptyLabelExceptError = 'No matches found on title...';
+    if (this.props.emptyLabelExceptError !== undefined) {
+      emptyLabelExceptError = this.props.emptyLabelExceptError;
+    }
+
+    return (this.state.searchError !== null)
+      ? 'Error on searching.'
+      : emptyLabelExceptError;
   }
 
   /**
@@ -142,26 +150,43 @@ export default class SearchTypeahead extends React.Component {
     );
   }
 
+  renderMenuItemChildren(option, props, index) {
+    const page = option;
+    return (
+      <span>
+      <UserPicture user={page.lastUpdateUser} size="sm" />
+      <PagePath page={page} />
+      <PageListMeta page={page} />
+      </span>
+    );
+  }
+
   render() {
-    const emptyLabel = (this.state.searchError !== null)
-      ? 'Error on searching.'
-      : 'No matches found on title...';
-    const restoreFormButton = this.getRestoreFormButton();
     const defaultSelected = (this.props.keywordOnInit != '')
       ? [{path: this.props.keywordOnInit}]
       : [];
+    const inputProps = { autoComplete: 'off' };
+    if (this.props.inputName != null) {
+      inputProps.name = this.props.inputName;
+    }
+
+    const restoreFormButton = this.getRestoreFormButton();
 
     return (
       <div className="search-typeahead">
         <AsyncTypeahead
           {...this.props}
           ref="typeahead"
-          inputProps={{name: 'q', autoComplete: 'off'}}
+          inputProps={inputProps}
           isLoading={this.state.isLoading}
           labelKey="path"
           minLength={0}
           options={this.state.pages} // Search result (Some page names)
-          emptyLabel={this.emptyLabel ? this.emptyLabel : emptyLabel}
+          emptyLabel={this.getEmptyLabel()}
+          searchText={(this.state.isLoading ? 'Searching...' : this.getEmptyLabel())}
+              // DIRTY HACK
+              //  note: The default searchText string has been shown wrongly even if isLoading is false
+              //        since upgrade react-bootstrap-typeahead to v3.3.2 -- 2019.02.05 Yuki Takei
           align='left'
           submitFormOnEnter={true}
           onSearch={this.search}
@@ -188,7 +213,9 @@ SearchTypeahead.propTypes = {
   onChange:        PropTypes.func,
   onSubmit:        PropTypes.func,
   onInputChange:   PropTypes.func,
+  inputName:       PropTypes.string,
   emptyLabel:      PropTypes.string,
+  emptyLabelExceptError: PropTypes.string,
   placeholder:     PropTypes.string,
   keywordOnInit:   PropTypes.string,
   promptText:      PropTypes.object,
@@ -201,7 +228,6 @@ SearchTypeahead.defaultProps = {
   onSearchSuccess: noop,
   onSearchError:   noop,
   onChange:        noop,
-  emptyLabel:      null,
   placeholder:     '',
   keywordOnInit:   '',
   onInputChange: () => {},

+ 3 - 0
src/client/styles/agile-admin/inverse/colors/_apply-colors-light.scss

@@ -28,6 +28,9 @@
  * GROWI search-top
  */
 .search-top {
+  .btn-group-dropdown-scope .dropdown-toggle {
+    background-color: rgba($bodycolor, 0.8);
+  }
   .rbt-input.form-control {
     background-color: rgba($bodycolor, 0.9);
   }

+ 29 - 27
src/client/styles/scss/_search.scss

@@ -20,7 +20,7 @@
   position: relative;
   .search-clear {
     position: absolute;
-    z-index: 2;
+    z-index: 3;
     top: 4px;
     right: 4px;
     width: 24px;
@@ -47,6 +47,7 @@
   }
 }
 
+
 // top and sidebar input styles
 .search-top, .search-sidebar {
   .search-clear {
@@ -54,29 +55,36 @@
     right: 26px;
   }
 
-  .input-group-btn {
-    position: absolute;
-    top: 0;
-    right: 0;
-    .btn {
-      padding: 4px 10px;
-    }
+  .btn-group-dropdown-scope .dropdown-toggle {
+    min-width: 95px;
+    height: 30px;
+    border-top-left-radius: 40px;
+    border-bottom-left-radius: 40px;
+    padding-top: 4px;
+    padding-bottom: 4px;
+    padding-right: 4px;
   }
-
   // using react-bootstrap-typeahead
   // see: https://github.com/ericgio/react-bootstrap-typeahead
   .rbt-input.form-control {
     border: none;
-    border-radius: 40px;
     border-top-right-radius: 40px;
     border-bottom-right-radius: 40px;
-    padding-top: 6px;
     height: 30px;
 
     .rbt-input-wrapper {
       margin-left: 8px;
     }
   }
+  .btn-group-submit-search {
+    position: absolute;
+    top: 0;
+    right: 0;
+    .btn {
+      padding: 4px 10px;
+    }
+    z-index: 3;
+  }
 }
 
 // layout
@@ -85,25 +93,24 @@
   margin-bottom: 10px;
 
   .rbt-input.form-control {
-    width: 180px;
+    width: 200px;
     transition: 0.3s ease-out;
     // focus
     &.focus {
       width: 300px;
     }
   }
-
-  table.search-help {
-    th, td {
-      border: none;
-    }
+  .rbt-menu {
+    margin-top: 33px;   // DIRTY HACK
+                        //   note: 'transform: translate3d(0px, XXpx, 0px)' calculation has failed on .search-top
+                        //         since upgrade react-bootstrap-typeahead to v3.3.2 -- 2019.02.05 Yuki Takei
   }
 }
 .search-sidebar {
   .search-form, .form-group, .rbt-input.form-control, .input-group {
     width: 100%;
   }
-  .input-group-btn {
+  .btn-group-submit-search {
     right: 30px;
   }
 }
@@ -169,18 +176,13 @@
   }
 }
 
-.search-page-input{
+.search-page-input {
   padding: 10px 0;
   position: sticky;
   top: 0;
   z-index: 99;
-  .form{
-    margin: 0;
-    .input-group .form-control{
-      height: 100%;
-    }
-    .input-group-btn .btn{
-      height: 34px;
-    }
+  .input-group-btn .btn{
+    padding: 0px 10px;
+    height: 34px;
   }
 }

+ 35 - 2
src/lib/util/page-path-utils.js

@@ -18,7 +18,40 @@ function encodePagePath(path) {
   return paths.join('/');
 }
 
+function matchEndWithSlash(path) {
+  // https://regex101.com/r/Z21fEd/1
+  return path.match(/(.+?)(\/)?$/);
+}
+
+function isEndWithSlash(path) {
+  const match = matchEndWithSlash(path);
+  return (match[2] != null);
+}
+
+function addSlashToTheEnd(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  if (!isEndWithSlash(path)) {
+    return `${path}/`;
+  }
+  return path;
+}
+
+function removeLastSlash(path) {
+  if (path === '/') {
+    return path;
+  }
+
+  const match = matchEndWithSlash(path);
+  return match[1];
+}
+
 module.exports = {
-  encodePagePath: encodePagePath,
-  encodePagesPath: encodePagesPath
+  encodePagePath,
+  encodePagesPath,
+  isEndWithSlash,
+  addSlashToTheEnd,
+  removeLastSlash,
 };

+ 9 - 8
src/server/routes/page.js

@@ -190,12 +190,13 @@ module.exports = function(crowi, app) {
     // check whether this page has portal page
     const portalPageStatus = await getPortalPageState(path, req.user);
 
+    let view = 'customlayout-selector/page_list';
     const renderVars = { path };
 
     if (portalPageStatus === PORTAL_STATUS_FORBIDDEN) {
       // inject to req
       req.isForbidden = true;
-      return next();
+      view = 'customlayout-selector/forbidden';
     }
     else if (portalPageStatus === PORTAL_STATUS_EXISTS) {
       let portalPage = await Page.findByPathAndViewer(path, req.user);
@@ -214,7 +215,7 @@ module.exports = function(crowi, app) {
     await addRenderVarsForDescendants(renderVars, path, req.user, offset, limit);
 
     await interceptorManager.process('beforeRenderPage', req, res, renderVars);
-    return res.render('customlayout-selector/page_list', renderVars);
+    return res.render(view, renderVars);
   }
 
   async function showPageForGrowiBehavior(req, res, next) {
@@ -621,8 +622,8 @@ module.exports = function(crowi, app) {
       options.grantUserGroupId = grantUserGroupId;
     }
 
-      const Revision = crowi.model('Revision');
-      const previousRevision = await Revision.findById(revisionId);
+    const Revision = crowi.model('Revision');
+    const previousRevision = await Revision.findById(revisionId);
     try {
       page = await Page.updatePage(page, pageBody, previousRevision.body, req.user, options);
     }
@@ -951,13 +952,13 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} path
    * @apiParam {String} revision_id
-   * @apiParam {String} q New path name.
+   * @apiParam {String} new_path New path name.
    * @apiParam {Bool} create_redirect
    */
   api.rename = async function(req, res) {
     const pageId = req.body.page_id;
     const previousRevision = req.body.revision_id || null;
-    const newPagePath = Page.normalizePath(req.body.q);
+    const newPagePath = Page.normalizePath(req.body.new_path);
     const options = {
       createRedirectPage: req.body.create_redirect || 0,
       moveUnderTrees: req.body.move_trees || 0,
@@ -1017,11 +1018,11 @@ module.exports = function(crowi, app) {
    * @apiGroup Page
    *
    * @apiParam {String} page_id Page Id.
-   * @apiParam {String} q New path name.
+   * @apiParam {String} new_path New path name.
    */
   api.duplicate = async function(req, res) {
     const pageId = req.body.page_id;
-    const newPagePath = Page.normalizePath(req.body.q);
+    const newPagePath = Page.normalizePath(req.body.new_path);
 
     const page = await Page.findByIdAndViewer(pageId, req.user);
 

+ 2 - 8
src/server/routes/search.js

@@ -32,7 +32,7 @@ module.exports = function(crowi, app) {
    */
   api.search = async function(req, res) {
     const user = req.user;
-    const { q: keyword = null, tree = null, type = null } = req.query;
+    const { q: keyword = null, type = null } = req.query;
     let paginateOpts;
 
     try {
@@ -61,13 +61,7 @@ module.exports = function(crowi, app) {
 
     const result = {};
     try {
-      let esResult;
-      if (tree) {
-        esResult = await search.searchKeywordUnderPath(keyword, tree, user, userGroups, searchOpts);
-      }
-      else {
-        esResult = await search.searchKeyword(keyword, user, userGroups, searchOpts);
-      }
+      const esResult = await search.searchKeyword(keyword, user, userGroups, searchOpts);
 
       // create score map for sorting
       // key: id , value: score

+ 61 - 71
src/server/util/search.js

@@ -437,42 +437,31 @@ SearchClient.prototype.initializeBoolQuery = function(query) {
   return query;
 };
 
-SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keyword) {
+SearchClient.prototype.appendCriteriaForQueryString = function(query, queryString) {
   query = this.initializeBoolQuery(query);
 
-  const appendMultiMatchQuery = function(query, type, keywords) {
-    let target;
-    let operator = 'and';
-    switch (type) {
-      case 'not_match':
-        target = query.body.query.bool.must_not;
-        operator = 'or';
-        break;
-      case 'match':
-      default:
-        target = query.body.query.bool.must;
-    }
+  // parse
+  let parsedKeywords = this.parseQueryString(queryString);
 
-    target.push({
+  if (parsedKeywords.match.length > 0) {
+    const q = {
       multi_match: {
-        query: keywords.join(' '),
-        // TODO: By user's i18n setting, change boost or search target fields
+        query: parsedKeywords.match.join(' '),
         fields: ['path.ja^2', 'path.en^2', 'body.ja', 'body.en'],
-        operator: operator,
       },
-    });
-
-    return query;
-  };
-
-  let parsedKeywords = this.getParsedKeywords(keyword);
-
-  if (parsedKeywords.match.length > 0) {
-    query = appendMultiMatchQuery(query, 'match', parsedKeywords.match);
+    };
+    query.body.query.bool.must.push(q);
   }
 
   if (parsedKeywords.not_match.length > 0) {
-    query = appendMultiMatchQuery(query, 'not_match', parsedKeywords.not_match);
+    const q = {
+      multi_match: {
+        query: parsedKeywords.not_match.join(' '),
+        fields: ['path.ja^2', 'path.en^2', 'body.ja', 'body.en'],
+        operator: 'or'
+      },
+    };
+    query.body.query.bool.must_not.push(q);
   }
 
   if (parsedKeywords.phrase.length > 0) {
@@ -512,19 +501,21 @@ SearchClient.prototype.appendCriteriaForKeywordContains = function(query, keywor
 
     query.body.query.bool.must_not.push(notPhraseQueries);
   }
-};
 
-SearchClient.prototype.appendCriteriaForPathFilter = function(query, path) {
-  query = this.initializeBoolQuery(query);
+  if (parsedKeywords.prefix.length > 0) {
+    const queries = parsedKeywords.prefix.map(path => {
+      return { prefix: { 'path.raw': path } };
+    });
+    query.body.query.bool.filter.push({ bool: { should: queries } });
+  }
 
-  if (path.match(/\/$/)) {
-    path = path.substr(0, path.length - 1);
+  if (parsedKeywords.not_prefix.length > 0) {
+    const queries = parsedKeywords.not_prefix.map(path => {
+      return { prefix: { 'path.raw': path } };
+    });
+    query.body.query.bool.filter.push({ bool: { must_not: queries } });
   }
-  query.body.query.bool.filter.push({
-    wildcard: {
-      'path.raw': path + '/*',
-    },
-  });
+
 };
 
 SearchClient.prototype.filterPagesByViewer = async function(query, user, userGroups) {
@@ -651,12 +642,12 @@ SearchClient.prototype.appendFunctionScore = function(query) {
   };
 };
 
-SearchClient.prototype.searchKeyword = async function(keyword, user, userGroups, option) {
+SearchClient.prototype.searchKeyword = async function(queryString, user, userGroups, option) {
   const from = option.offset || null;
   const size = option.limit || null;
   const type = option.type || null;
   const query = this.createSearchQuerySortedByScore();
-  this.appendCriteriaForKeywordContains(query, keyword);
+  this.appendCriteriaForQueryString(query, queryString);
 
   this.filterPagesByType(query, type);
   await this.filterPagesByViewer(query, user, userGroups);
@@ -668,43 +659,23 @@ SearchClient.prototype.searchKeyword = async function(keyword, user, userGroups,
   return this.search(query);
 };
 
-SearchClient.prototype.searchByPath = async function(keyword, prefix) {
-  // TODO path 名だけから検索
-};
-
-SearchClient.prototype.searchKeywordUnderPath = async function(keyword, path, user, userGroups, option) {
-  const from = option.offset || null;
-  const size = option.limit || null;
-  const type = option.type || null;
-  const query = this.createSearchQuerySortedByScore();
-  this.appendCriteriaForKeywordContains(query, keyword);
-  this.appendCriteriaForPathFilter(query, path);
-
-  this.filterPagesByType(query, type);
-  await this.filterPagesByViewer(query, user, userGroups);
-
-  this.appendResultSize(query, from, size);
-
-  this.appendFunctionScore(query);
-
-  return this.search(query);
-};
-
-SearchClient.prototype.getParsedKeywords = function(keyword) {
+SearchClient.prototype.parseQueryString = function(queryString) {
   let matchWords = [];
   let notMatchWords = [];
   let phraseWords = [];
   let notPhraseWords = [];
+  let prefixPaths = [];
+  let notPrefixPaths = [];
 
-  keyword.trim();
-  keyword = keyword.replace(/\s+/g, ' ');
+  queryString.trim();
+  queryString = queryString.replace(/\s+/g, ' ');
 
   // First: Parse phrase keywords
   let phraseRegExp = new RegExp(/(-?"[^"]+")/g);
-  let phrases = keyword.match(phraseRegExp);
+  let phrases = queryString.match(phraseRegExp);
 
   if (phrases !== null) {
-    keyword = keyword.replace(phraseRegExp, '');
+    queryString = queryString.replace(phraseRegExp, '');
 
     phrases.forEach(function(phrase) {
       phrase.trim();
@@ -718,16 +689,33 @@ SearchClient.prototype.getParsedKeywords = function(keyword) {
   }
 
   // Second: Parse other keywords (include minus keywords)
-  keyword.split(' ').forEach(function(word) {
+  queryString.split(' ').forEach(function(word) {
     if (word === '') {
       return;
     }
 
-    if (word.match(/^-(.+)$/)) {
-      notMatchWords.push(RegExp.$1);
+    // https://regex101.com/r/lN4LIV/1
+    const matchNegative = word.match(/^-(prefix:)?(.+)$/);
+    // https://regex101.com/r/gVssZe/1
+    const matchPositive = word.match(/^(prefix:)?(.+)$/);
+
+    if (matchNegative != null) {
+      const isPrefixCondition = (matchNegative[1] != null);
+      if (isPrefixCondition) {
+        notPrefixPaths.push(matchNegative[2]);
+      }
+      else {
+        notMatchWords.push(matchNegative[2]);
+      }
     }
-    else {
-      matchWords.push(word);
+    else if (matchPositive != null) {
+      const isPrefixCondition = (matchPositive[1] != null);
+      if (isPrefixCondition) {
+        prefixPaths.push(matchPositive[2]);
+      }
+      else {
+        matchWords.push(matchPositive[2]);
+      }
     }
   });
 
@@ -736,6 +724,8 @@ SearchClient.prototype.getParsedKeywords = function(keyword) {
     not_match: notMatchWords,
     phrase: phraseWords,
     not_phrase: notPhraseWords,
+    prefix: prefixPaths,
+    not_prefix: notPrefixPaths,
   };
 };
 

+ 0 - 14
src/server/views/layout-crowi/page_list.html

@@ -18,20 +18,6 @@
       <div class="title-container">
         <div class="d-flex">
           <h1 class="title" id="revision-path"></h1>
-          {% if false %} {# Disable temporaly -- 2018.03.08 Yuki Takei #}
-          {% if searchConfigured() && !isTopPage() && !isTrashPage() %}
-          <form id="search-listpage-form" class="m-l-10 input-group search-input-group hidden-xs hidden-sm"
-              data-toggle="tooltip" data-placement="bottom" title="{{ path }} 以下から検索" data-container="body">
-            <div class="input-group">
-              <input id="search-listpage-input" type="text" class="form-control input-sm" data-path="{{ path }}" placeholder="Search for...">
-              <span class="input-group-btn">
-                <button class="btn btn-default btn-sm"><i class="icon-magnifier"></i></button>
-              </span>
-            </div><!-- /input-group -->
-            <a class="search-listpage-clear" id="search-listpage-clear"><i class="fa fa-times-circle"></i></a>
-          </form>
-          {% endif %}
-          {% endif %}
         </div>
         <div id="revision-url" class="url-line"></div>
       </div>

+ 1 - 1
src/server/views/layout-growi/forbidden.html

@@ -2,7 +2,7 @@
 
 
 {% block content_header %}
-  {% include 'widget/header.html' %}
+  {% include 'widget/header.html' with {forbidden: true} %}
 {% endblock %}
 
 

+ 1 - 1
src/server/views/layout-growi/widget/header.html

@@ -39,7 +39,7 @@
       </ul>
       {% endif %}
 
-      {% if not page and ('/' === path or 'crowi' === behaviorType()) and not isUserPageList(path) and !isTrashPage() %}
+      {% if not page and not forbidden and ('/' === path or 'crowi' === behaviorType()) and not isUserPageList(path) and !isTrashPage() %}
         {% if '/' === path.slice(-1) %}
           {% include '../../widget/create_portal.html' %}
         {% endif %}

+ 1 - 1
src/server/views/modal/duplicate.html

@@ -20,7 +20,7 @@
                 {% if searchConfigured() %}
                 <div id="duplicate-page-name-input" class="page-name-input"></div>
                 {% else %}
-                <input type="text" class="form-control" name="q" id="duplicatePageName" value="{{ page.path }}">
+                <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
                 {% endif %}
               </div>
             </div>

+ 1 - 1
src/server/views/modal/rename.html

@@ -20,7 +20,7 @@
               {% if searchConfigured() %}
               <div id="rename-page-name-input" class="page-name-input"></div>
               {% else %}
-              <input type="text" class="form-control" name="q" id="newPageName" value="{{ page.path }}">
+              <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
               {% endif %}
             </div>
           </div>

+ 97 - 22
yarn.lock

@@ -11,6 +11,13 @@
     loader-utils "^1.1.0"
     lodash "^4.17.10"
 
+"@babel/runtime@^7.1.2":
+  version "7.3.1"
+  resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.3.1.tgz#574b03e8e8a9898eaf4a872a92ea20b7846f6f2a"
+  integrity sha512-7jGW8ppV0ant637pIqAcFfQDDH1orEPGJb8aXfUozuCU3QqX7rX4DA8iwrbPrR1hcH0FTTHz47yQnk+bl5xHQA==
+  dependencies:
+    regenerator-runtime "^0.12.0"
+
 "@browser-bunyan/console-formatted-stream@^1.3.0":
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/@browser-bunyan/console-formatted-stream/-/console-formatted-stream-1.3.0.tgz#3dc059aa5c1b2a7a1f26e2706e2bdeb9a09bbe57"
@@ -2341,6 +2348,22 @@ create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4, create-hmac@^1.1.6:
     safe-buffer "^5.0.1"
     sha.js "^2.4.8"
 
+create-react-context@<=0.2.2:
+  version "0.2.2"
+  resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.2.tgz#9836542f9aaa22868cd7d4a6f82667df38019dca"
+  integrity sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==
+  dependencies:
+    fbjs "^0.8.0"
+    gud "^1.0.0"
+
+create-react-context@^0.2.3:
+  version "0.2.3"
+  resolved "https://registry.yarnpkg.com/create-react-context/-/create-react-context-0.2.3.tgz#9ec140a6914a22ef04b8b09b7771de89567cb6f3"
+  integrity sha512-CQBmD0+QGgTaxDL3OX1IDXYqjkp2It4RIbcb99jS6AEg27Ga+a9G3JtK6SIu0HBwPLZlmwt9F7UwWA4Bn92Rag==
+  dependencies:
+    fbjs "^0.8.0"
+    gud "^1.0.0"
+
 cross-env@^5.0.5:
   version "5.1.3"
   resolved "https://registry.yarnpkg.com/cross-env/-/cross-env-5.1.3.tgz#f8ae18faac87692b0a8b4d2f7000d4ec3a85dfd7"
@@ -3528,6 +3551,19 @@ fastparse@^1.1.1:
   version "1.1.1"
   resolved "https://registry.yarnpkg.com/fastparse/-/fastparse-1.1.1.tgz#d1e2643b38a94d7583b479060e6c4affc94071f8"
 
+fbjs@^0.8.0:
+  version "0.8.17"
+  resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.17.tgz#c4d598ead6949112653d6588b01a5cdcd9f90fdd"
+  integrity sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=
+  dependencies:
+    core-js "^1.0.0"
+    isomorphic-fetch "^2.1.1"
+    loose-envify "^1.0.0"
+    object-assign "^4.1.0"
+    promise "^7.1.1"
+    setimmediate "^1.0.5"
+    ua-parser-js "^0.7.18"
+
 fbjs@^0.8.16:
   version "0.8.16"
   resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db"
@@ -4102,6 +4138,11 @@ gtoken@^2.3.0:
     mime "^2.2.0"
     pify "^3.0.0"
 
+gud@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/gud/-/gud-1.0.0.tgz#a489581b17e6a70beca9abe3ae57de7a499852c0"
+  integrity sha512-zGEOVKFM5sVPPrYs7J5/hYEw2Pof8KCyOwyhG8sAF26mCAeUFAcYPu1mwB7hhpIP29zOIBaDqwuHdLp0jvZXjw==
+
 gzip-size@^5.0.0:
   version "5.0.0"
   resolved "https://registry.yarnpkg.com/gzip-size/-/gzip-size-5.0.0.tgz#a55ecd99222f4c48fd8c01c625ce3b349d0a0e80"
@@ -6936,9 +6977,10 @@ pluralize@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
 
-popper.js@^1.14.1:
-  version "1.14.3"
-  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
+popper.js@^1.14.4:
+  version "1.14.7"
+  resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.7.tgz#e31ec06cfac6a97a53280c3e55e4e0c860e7738e"
+  integrity sha512-4q1hNvoUre/8srWsH7hnoSJ5xVmIL4qgz+s4qf2TnJIMyZFUFMGH+9vE7mXynAlHSZ/NdTmmow86muD0myUkVQ==
 
 portscanner@2.1.1:
   version "2.1.1"
@@ -7482,20 +7524,21 @@ rc@^1.1.7:
     minimist "^1.2.0"
     strip-json-comments "~2.0.1"
 
-react-bootstrap-typeahead@^3.1.5:
-  version "3.1.5"
-  resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.1.5.tgz#4761300571334ca447d7c7e9b85864205fccd8c0"
+react-bootstrap-typeahead@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/react-bootstrap-typeahead/-/react-bootstrap-typeahead-3.3.2.tgz#ffe0193b6fc4af585d1724b1447d8bf2c871f75a"
+  integrity sha512-BCaFXrN3MefKj2jn8HqeSHu4hWwv1f8WtYWTNOhkz+KN4QtNLwbm7ttxOVuaG8CWk89HYGvF6QEjN5XIFE47hg==
   dependencies:
     classnames "^2.2.0"
+    create-react-context "^0.2.3"
     escape-string-regexp "^1.0.5"
     invariant "^2.2.1"
     lodash "^4.17.2"
     prop-types "^15.5.8"
     prop-types-extra "^1.0.1"
-    react-onclickoutside "^6.1.1"
     react-overlays "^0.8.1"
-    react-popper "^0.10.4"
-    warning "^3.0.0"
+    react-popper "^1.0.0"
+    warning "^4.0.1"
 
 react-bootstrap@^0.32.1:
   version "0.32.1"
@@ -7557,10 +7600,6 @@ react-is@^16.6.3:
   version "16.6.3"
   resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.6.3.tgz#d2d7462fcfcbe6ec0da56ad69047e47e56e7eac0"
 
-react-onclickoutside@^6.1.1:
-  version "6.7.0"
-  resolved "https://registry.yarnpkg.com/react-onclickoutside/-/react-onclickoutside-6.7.0.tgz#997a4d533114c9a0a104913638aa26afc084f75c"
-
 react-overlays@^0.8.0, react-overlays@^0.8.1:
   version "0.8.3"
   resolved "https://registry.yarnpkg.com/react-overlays/-/react-overlays-0.8.3.tgz#fad65eea5b24301cca192a169f5dddb0b20d3ac5"
@@ -7572,12 +7611,17 @@ react-overlays@^0.8.0, react-overlays@^0.8.1:
     react-transition-group "^2.2.0"
     warning "^3.0.0"
 
-react-popper@^0.10.4:
-  version "0.10.4"
-  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-0.10.4.tgz#af2a415ea22291edd504678d7afda8a6ee3295aa"
+react-popper@^1.0.0:
+  version "1.3.3"
+  resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-1.3.3.tgz#2c6cef7515a991256b4f0536cd4bdcb58a7b6af6"
+  integrity sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==
   dependencies:
-    popper.js "^1.14.1"
+    "@babel/runtime" "^7.1.2"
+    create-react-context "<=0.2.2"
+    popper.js "^1.14.4"
     prop-types "^15.6.1"
+    typed-styles "^0.0.7"
+    warning "^4.0.2"
 
 react-prop-types@^0.4.0:
   version "0.4.0"
@@ -7604,14 +7648,15 @@ react-waypoint@^8.1.0:
     prop-types "^15.0.0"
     react-is "^16.6.3"
 
-react@^16.4.1:
-  version "16.4.1"
-  resolved "https://registry.yarnpkg.com/react/-/react-16.4.1.tgz#de51ba5764b5dbcd1f9079037b862bd26b82fe32"
+react@^16.7.0:
+  version "16.7.0"
+  resolved "https://registry.yarnpkg.com/react/-/react-16.7.0.tgz#b674ec396b0a5715873b350446f7ea0802ab6381"
+  integrity sha512-StCz3QY8lxTb5cl2HJxjwLFOXPIFQp+p+hxQfc8WE0QiLfCtIlKj8/+5tjjKm8uSTlAW+fCPaavGFS06V9Ar3A==
   dependencies:
-    fbjs "^0.8.16"
     loose-envify "^1.1.0"
     object-assign "^4.1.1"
-    prop-types "^15.6.0"
+    prop-types "^15.6.2"
+    scheduler "^0.12.0"
 
 read-pkg-up@^1.0.1:
   version "1.0.1"
@@ -7755,6 +7800,11 @@ regenerator-runtime@^0.11.0:
   version "0.11.1"
   resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9"
 
+regenerator-runtime@^0.12.0:
+  version "0.12.1"
+  resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz#fa1a71544764c036f8c49b13a08b2594c9f8a0de"
+  integrity sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==
+
 regenerator-transform@^0.10.0:
   version "0.10.1"
   resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"
@@ -8173,6 +8223,14 @@ sax@>=0.6.0, sax@^1.2.4, sax@~1.2.4:
   version "1.2.4"
   resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9"
 
+scheduler@^0.12.0:
+  version "0.12.0"
+  resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.12.0.tgz#8ab17699939c0aedc5a196a657743c496538647b"
+  integrity sha512-t7MBR28Akcp4Jm+QoR63XgAi9YgCUmgvDHqf5otgAj4QvdoBE4ImCX0ffehefePPG+aitiYHp0g/mW6s4Tp+dw==
+  dependencies:
+    loose-envify "^1.1.0"
+    object-assign "^4.1.1"
+
 schema-utils@^0.4.4, schema-utils@^0.4.5:
   version "0.4.5"
   resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-0.4.5.tgz#21836f0608aac17b78f9e3e24daff14a5ca13a3e"
@@ -9149,6 +9207,11 @@ type-is@~1.6.16:
     media-typer "0.3.0"
     mime-types "~2.1.18"
 
+typed-styles@^0.0.7:
+  version "0.0.7"
+  resolved "https://registry.yarnpkg.com/typed-styles/-/typed-styles-0.0.7.tgz#93392a008794c4595119ff62dde6809dbc40a3d9"
+  integrity sha512-pzP0PWoZUhsECYjABgCGQlRGL1n7tOHsgwYv3oIiEpJwGhFTuty/YNeduxQYzXXa3Ge5BdT6sHYIQYpl4uJ+5Q==
+
 typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
@@ -9157,6 +9220,11 @@ ua-parser-js@0.7.12:
   version "0.7.12"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.12.tgz#04c81a99bdd5dc52263ea29d24c6bf8d4818a4bb"
 
+ua-parser-js@^0.7.18:
+  version "0.7.19"
+  resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.19.tgz#94151be4c0a7fb1d001af7022fdaca4642659e4b"
+  integrity sha512-T3PVJ6uz8i0HzPxOF9SWzWAlfN/DavlpQqepn22xgve/5QecC+XMCAtmUNnY7C9StehaV6exjUCI801lOI7QlQ==
+
 ua-parser-js@^0.7.9:
   version "0.7.17"
   resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac"
@@ -9472,6 +9540,13 @@ warning@^3.0.0:
   dependencies:
     loose-envify "^1.0.0"
 
+warning@^4.0.1, warning@^4.0.2:
+  version "4.0.2"
+  resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.2.tgz#aa6876480872116fa3e11d434b0d0d8d91e44607"
+  integrity sha512-wbTp09q/9C+jJn4KKJfJfoS6VleK/Dti0yqWSm6KMvJ4MRCXFQNapHuJXutJIrWV0Cf4AhTdeIe4qdKHR1+Hug==
+  dependencies:
+    loose-envify "^1.0.0"
+
 watchpack@^1.5.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.0.tgz#4bc12c2ebe8aa277a71f1d3f14d685c7b446cd00"