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

Merge branch 'master' into imprv/refactor-attachment

Yuki Takei 7 лет назад
Родитель
Сommit
5e4d45c590
31 измененных файлов с 499 добавлено и 621 удалено
  1. 9 1
      CHANGES.md
  2. 1 1
      package.json
  3. 32 0
      resource/locales/en-US/translation.json
  4. 33 0
      resource/locales/ja/translation.json
  5. 5 5
      src/client/js/app.js
  6. 0 37
      src/client/js/components/HeaderSearchBox.js
  7. 99 0
      src/client/js/components/HeaderSearchBox.jsx
  8. 0 115
      src/client/js/components/HeaderSearchBox/SearchForm.js
  9. 0 61
      src/client/js/components/HeaderSearchBox/SearchSuggest.js
  10. 0 169
      src/client/js/components/PageListSearch.js
  11. 112 0
      src/client/js/components/SearchForm.js
  12. 7 3
      src/client/js/components/SearchPage.js
  13. 0 61
      src/client/js/components/SearchPage/SearchForm.js
  14. 61 0
      src/client/js/components/SearchPage/SearchPageForm.js
  15. 5 12
      src/client/js/components/SearchTypeahead.js
  16. 4 4
      src/client/js/legacy/crowi.js
  17. 3 0
      src/client/styles/agile-admin/inverse/colors/_apply-colors-light.scss
  18. 3 4
      src/client/styles/scss/_comment_growi.scss
  19. 1 1
      src/client/styles/scss/_create-page.scss
  20. 20 0
      src/client/styles/scss/_editor-navbar.scss
  21. 0 19
      src/client/styles/scss/_mixins.scss
  22. 24 27
      src/client/styles/scss/_search.scss
  23. 1 0
      src/client/styles/scss/style.scss
  24. 4 4
      src/server/routes/page.js
  25. 2 8
      src/server/routes/search.js
  26. 61 71
      src/server/util/search.js
  27. 0 14
      src/server/views/layout-crowi/page_list.html
  28. 1 1
      src/server/views/modal/create_page.html
  29. 5 1
      src/server/views/modal/duplicate.html
  30. 5 1
      src/server/views/modal/rename.html
  31. 1 1
      src/server/views/modal/unportalize.html

+ 9 - 1
CHANGES.md

@@ -1,11 +1,19 @@
 CHANGES
 ========
 
-## 3.3.7-RC
+## 3.3.8-RC
+
+* 
+
+## 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

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.3.7-RC",
+  "version": "3.3.8-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",

+ 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": "これは現在の版ではありません。",

+ 5 - 5
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';
@@ -273,17 +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" />,
 
-  'page-name-input': <NewPageNameInput crowi={crowi} parentPageName={pagePath} />,
+  '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} />,
 
 };
 // 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 SearchForm from './HeaderSearchBox/SearchForm';
-// 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">
-        <SearchForm />
-        {/* 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 - 115
src/client/js/components/HeaderSearchBox/SearchForm.js

@@ -1,115 +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 SearchTypeahead from '../SearchTypeahead';
-
-
-// Header.SearchForm
-export default class SearchForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.crowi = window.crowi; // FIXME
-
-    this.state = {
-      searchError: null,
-    };
-
-    this.onSearchError = this.onSearchError.bind(this);
-    this.onChange = this.onChange.bind(this);
-    this.onSubmit = this.onSubmit.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
-  }
-
-  onSearchError(err) {
-    this.setState({
-      searchError: err,
-    });
-  }
-
-  onChange(selected) {
-    const page = selected[0];  // should be single page selected
-
-    // navigate to page
-    if (page != null) {
-      window.location = page.path;
-    }
-  }
-
-  getHelpElement() {
-    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>
-        </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>
-          </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>
-          </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>
-          </tr>
-        </tbody>
-      </table>
-    );
-  }
-
-  onSubmit(query) {
-    this.refs.form.submit(query);
-  }
-
-  render() {
-    const emptyLabel = (this.state.searchError !== null)
-      ? 'Error on searching.'
-      : 'No matches found on title... Hit [Enter] key so that search on contents.';
-
-    return (
-      <form
-        ref='form'
-        action='/_search'
-        className='search-form form-group input-group search-input-group hidden-print'
-      >
-        <FormGroup>
-          <InputGroup>
-            <SearchTypeahead
-              crowi={this.crowi}
-              onChange={this.onChange}
-              onSubmit={this.onSubmit}
-              emptyLabel={emptyLabel}
-              placeholder="Search ..."
-              promptText={this.getHelpElement()}
-            />
-            <InputGroup.Button>
-              <Button type="submit" bsStyle="link">
-                <i className="icon-magnifier"></i>
-              </Button >
-            </InputGroup.Button>
-          </InputGroup>
-        </FormGroup>
-
-      </form>
-
-    );
-  }
-}
-
-SearchForm.propTypes = {
-};
-
-SearchForm.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 - 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 || {}),
-};
-

+ 112 - 0
src/client/js/components/SearchForm.js

@@ -0,0 +1,112 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import SearchTypeahead from './SearchTypeahead';
+
+// SearchTypeahead wrapper
+export default class SearchForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      searchError: null,
+    };
+
+    this.onSearchError = this.onSearchError.bind(this);
+    this.onChange = this.onChange.bind(this);
+  }
+
+  componentDidMount() {
+  }
+
+  componentWillUnmount() {
+  }
+
+  onSearchError(err) {
+    this.setState({
+      searchError: err,
+    });
+  }
+
+  onChange(selected) {
+    const page = selected[0];  // should be single page selected
+
+    // navigate to page
+    if (page != null) {
+      window.location = page.path;
+    }
+  }
+
+  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"/>{ t('search_help.title') }</h5>
+        </caption>
+        <tbody>
+          <tr>
+            <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>
+            <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>
+            <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>
+    );
+  }
+
+  render() {
+    const t = this.props.t;
+    const emptyLabel = (this.state.searchError !== null)
+      ? 'Error on searching.'
+      : t('search.search page bodies');
+
+    return (
+      <SearchTypeahead
+        crowi={this.props.crowi}
+        onChange={this.onChange}
+        onSubmit={this.props.onSubmit}
+        onInputChange={this.props.onInputChange}
+        onSearchError={this.onSearchError}
+        emptyLabel={emptyLabel}
+        placeholder="Search ..."
+        promptText={this.getHelpElement()}
+        keywordOnInit={this.props.keyword}
+      />
+    );
+  }
+}
+
+SearchForm.propTypes = {
+  t: PropTypes.func.isRequired,               // i18next
+  crowi: PropTypes.object.isRequired,
+  keyword: PropTypes.string,
+  onSubmit: PropTypes.func.isRequired,
+  onInputChange: PropTypes.func,
+};
+
+SearchForm.defaultProps = {
+  onInputChange: () => {},
+};

+ 7 - 3
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 SearchForm from './SearchPage/SearchForm';
+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,8 @@ export default class SearchPage extends React.Component {
     return (
       <div>
         <div className="search-page-input">
-          <SearchForm
+          <SearchPageForm t={this.props.t}
+            crowi={this.props.crowi}
             onSearchFormChanged={this.search}
             keyword={this.state.searchingKeyword}
             />
@@ -109,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,
@@ -119,3 +122,4 @@ SearchPage.defaultProps = {
   searchError: null,
 };
 
+export default translate()(SearchPage);

+ 0 - 61
src/client/js/components/SearchPage/SearchForm.js

@@ -1,61 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-// Search.SearchForm
-export default class SearchForm extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      keyword: this.props.keyword,
-      searchedKeyword: this.props.keyword,
-    };
-
-    this.handleSubmit = this.handleSubmit.bind(this);
-    this.handleChange = this.handleChange.bind(this);
-  }
-
-  search() {
-    if (this.state.searchedKeyword != this.state.keyword) {
-      this.props.onSearchFormChanged({keyword: this.state.keyword});
-      this.setState({searchedKeyword: this.state.keyword});
-    }
-  }
-
-  handleSubmit(event) {
-    event.preventDefault();
-    this.search({keyword: this.state.keyword});
-  }
-
-  handleChange(event) {
-    const keyword = event.target.value;
-    this.setState({keyword});
-  }
-
-  render() {
-    return (
-      <form className="form form-group input-group" onSubmit={this.handleSubmit}>
-        <input
-          type="text"
-          name="q"
-          value={this.state.keyword}
-          onChange={this.handleChange}
-          className="form-control"
-          />
-          <span className="input-group-btn">
-            <button type="submit" className="btn btn-default">
-              <i className="search-top-icon icon-magnifier"></i>
-            </button>
-          </span>
-      </form>
-    );
-  }
-}
-
-SearchForm.propTypes = {
-  keyword: PropTypes.string,
-  onSearchFormChanged: PropTypes.func.isRequired,
-};
-SearchForm.defaultProps = {
-};

+ 61 - 0
src/client/js/components/SearchPage/SearchPageForm.js

@@ -0,0 +1,61 @@
+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
+export default class SearchPageForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      keyword: this.props.keyword,
+      searchedKeyword: this.props.keyword,
+    };
+
+    this.search = this.search.bind(this);
+    this.onInputChange = this.onInputChange.bind(this);
+  }
+
+  search() {
+    const keyword = this.state.keyword;
+    this.props.onSearchFormChanged({keyword: keyword});
+    this.setState({searchedKeyword: keyword});
+  }
+
+  onInputChange(input) { // for only submitting with button
+    this.setState({keyword: input});
+  }
+
+  render() {
+    return <FormGroup>
+      <InputGroup>
+        <SearchForm t={this.props.t}
+          crowi={this.props.crowi}
+          onSubmit={this.search}
+          keyword={this.state.searchedKeyword}
+          onInputChange={this.onInputChange}
+        />
+        <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,
+};
+SearchPageForm.defaultProps = {
+};

+ 5 - 12
src/client/js/components/SearchTypeahead.js

@@ -28,7 +28,6 @@ export default class SearchTypeahead extends React.Component {
     this.search = this.search.bind(this);
     this.onInputChange = this.onInputChange.bind(this);
     this.onKeyDown = this.onKeyDown.bind(this);
-    this.onChange = this.onChange.bind(this);
     this.dispatchSubmit = this.dispatchSubmit.bind(this);
     this.getRestoreFormButton = this.getRestoreFormButton.bind(this);
     this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
@@ -93,6 +92,7 @@ export default class SearchTypeahead extends React.Component {
 
   onInputChange(text) {
     this.setState({input: text});
+    this.props.onInputChange(text);
     if (text === '') {
       this.setState({pages: []});
     }
@@ -104,18 +104,9 @@ export default class SearchTypeahead extends React.Component {
     }
   }
 
-  onChange(selected) {
-    const page = selected[0];  // should be single page selected
-
-    // navigate to page
-    if (page != null) {
-      window.location = page.path;
-    }
-  }
-
   dispatchSubmit() {
     if (this.props.onSubmit != null) {
-      this.props.onSubmit(this.state.keyword);
+      this.props.onSubmit(this.state.input);
     }
   }
 
@@ -165,7 +156,7 @@ export default class SearchTypeahead extends React.Component {
         <AsyncTypeahead
           {...this.props}
           ref="typeahead"
-          inputProps={{name: 'q', autoComplete: 'off'}}
+          inputProps={{autoComplete: 'off'}}
           isLoading={this.state.isLoading}
           labelKey="path"
           minLength={0}
@@ -196,6 +187,7 @@ SearchTypeahead.propTypes = {
   onSearchError:   PropTypes.func,
   onChange:        PropTypes.func,
   onSubmit:        PropTypes.func,
+  onInputChange:   PropTypes.func,
   emptyLabel:      PropTypes.string,
   placeholder:     PropTypes.string,
   keywordOnInit:   PropTypes.string,
@@ -212,4 +204,5 @@ SearchTypeahead.defaultProps = {
   emptyLabel:      null,
   placeholder:     '',
   keywordOnInit:   '',
+  onInputChange: () => {},
 };

+ 4 - 4
src/client/js/legacy/crowi.js

@@ -357,7 +357,7 @@ $(function() {
     // create name-value map
     let nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value;
+      nameValueMap[obj.name] = obj.value; // nameValueMap['q'] is renamed page path
     });
 
     const data = $(this).serialize() + `&socketClientId=${crowi.getSocketClientId()}`;
@@ -374,7 +374,7 @@ $(function() {
         $('#renamePage .msg, #unportalize .msg').hide();
         $(`#renamePage .msg-${res.code}, #unportalize .msg-${res.code}`).show();
         $('#renamePage #linkToNewPage, #unportalize #linkToNewPage').html(`
-          <a href="${nameValueMap.new_path}">${nameValueMap.new_path} <i class="icon-login"></i></a>
+          <a href="${nameValueMap.q}">${nameValueMap.q} <i class="icon-login"></i></a>
         `);
       }
       else {
@@ -395,7 +395,7 @@ $(function() {
     // create name-value map
     let nameValueMap = {};
     $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value;
+      nameValueMap[obj.name] = obj.value; // nameValueMap['q'] is duplicated page path
     });
 
     $.ajax({
@@ -409,7 +409,7 @@ $(function() {
         $('#duplicatePage .msg').hide();
         $(`#duplicatePage .msg-${res.code}`).show();
         $('#duplicatePage #linkToNewPage').html(`
-          <a href="${nameValueMap.new_path}">${nameValueMap.new_path} <i class="icon-login"></i></a>
+          <a href="${nameValueMap.q}">${nameValueMap.q} <i class="icon-login"></i></a>
         `);
       }
       else {

+ 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);
   }

+ 3 - 4
src/client/styles/scss/_comment_growi.scss

@@ -100,7 +100,6 @@
     }
 
 
-
     position: relative;
     margin-top: 2em;
     // user icon
@@ -117,9 +116,6 @@
     .comment-write {
       margin-bottom: 0.5em;
     }
-    .tab-content{
-      padding-top: 10px;
-    }
     .comment-form-comment {
       height: 80px;
       &:focus, &:not(:invalid) {
@@ -127,5 +123,8 @@
         height: 180px;
       }
     }
+    .comment-form-preview {
+      padding-top: 0.5em;
+    }
   }
 }

+ 1 - 1
src/client/styles/scss/_create-page.scss

@@ -51,7 +51,7 @@
         margin-left: 5px;
       }
 
-      #page-name-input {
+      .page-name-input {
         flex: 1;
         input {
           min-width: 300px; // Workaround to display placeholder.

+ 20 - 0
src/client/styles/scss/_editor-navbar.scss

@@ -0,0 +1,20 @@
+.editor-container {
+  .navbar-editor {
+    li {
+      display: inline-block;
+    }
+
+    button {
+      margin: 0 2px;
+      padding: 8px;
+      border: none;
+      background-color: transparent;
+      font-size: 14px;
+      line-height: 1;
+    }
+
+    img {
+      vertical-align: bottom;
+    }
+  }
+}

+ 0 - 19
src/client/styles/scss/_mixins.scss

@@ -50,25 +50,6 @@
           height: calc(100vh - #{$header-plus-footer});
         }
 
-        .navbar-editor {
-          li {
-            display: inline-block;
-          }
-
-          button {
-            margin: 0 2px;
-            padding: 8px;
-            border: none;
-            background-color: transparent;
-            font-size: 14px;
-            line-height: 1;
-          }
-
-          img {
-            vertical-align: bottom;
-          }
-        }
-
         // left(editor)
         .page-editor-editor-container {
           min-height: calc(100vh - #{$header-plus-footer});   // for IE11

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

@@ -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,19 @@
   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;
-    }
-  }
 }
 .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 +171,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;
   }
 }

+ 1 - 0
src/client/styles/scss/style.scss

@@ -24,6 +24,7 @@
 @import 'create-page';
 @import 'create-template';
 @import 'editor-attachment';
+@import 'editor-navbar';
 @import 'layout';
 @import 'layout_crowi';
 @import 'layout_crowi_sidebar';

+ 4 - 4
src/server/routes/page.js

@@ -951,13 +951,13 @@ module.exports = function(crowi, app) {
    * @apiParam {String} page_id Page Id.
    * @apiParam {String} path
    * @apiParam {String} revision_id
-   * @apiParam {String} new_path
+   * @apiParam {String} q 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.new_path);
+    const newPagePath = Page.normalizePath(req.body.q);
     const options = {
       createRedirectPage: req.body.create_redirect || 0,
       moveUnderTrees: req.body.move_trees || 0,
@@ -1017,11 +1017,11 @@ module.exports = function(crowi, app) {
    * @apiGroup Page
    *
    * @apiParam {String} page_id Page Id.
-   * @apiParam {String} new_path
+   * @apiParam {String} q New path name.
    */
   api.duplicate = async function(req, res) {
     const pageId = req.body.page_id;
-    const newPagePath = Page.normalizePath(req.body.new_path);
+    const newPagePath = Page.normalizePath(req.body.q);
 
     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/modal/create_page.html

@@ -32,7 +32,7 @@
             <div class="d-flex create-page-input-container">
               <div class="create-page-input-row d-flex align-items-center">
                 {% if searchConfigured() %}
-                <div id="page-name-input"></div>
+                <div id="create-page-name-input" class="page-name-input"></div>
                 {% else %}
                 <input type="text" value="{{ parentPath(path) }}" class="page-name-input form-control " placeholder="{{ t('Input page name') }}" required />
                 {% endif %}

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

@@ -17,7 +17,11 @@
               <label for="duplicatePageName">{{ t('modal_duplicate.label.New page name') }}</label><br>
               <div class="input-group">
                 <span class="input-group-addon">{{ baseUrl }}</span>
-                <input type="text" class="form-control" name="new_path" id="duplicatePageName" value="{{ page.path }}">
+                {% 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 }}">
+                {% endif %}
               </div>
             </div>
         </div>

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

@@ -17,7 +17,11 @@
             <label for="newPageName">{{ t('modal_rename.label.New page name') }}</label><br>
             <div class="input-group">
               <span class="input-group-addon">{{ baseUrl }}</span>
-              <input type="text" class="form-control" name="new_path" id="newPageName" value="{{ page.path }}">
+              {% 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 }}">
+              {% endif %}
             </div>
           </div>
 

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

@@ -37,7 +37,7 @@
             <div>
               <input type="hidden" name="_csrf" value="{{ csrf() }}">
               <input type="hidden" name="path" value="{{ page.path }}">
-              <input type="hidden" class="form-control" name="new_path" id="newPageName" value="{{ unportalizedPath }}">
+              <input type="hidden" class="form-control" name="q" id="newPageName" value="{{ unportalizedPath }}">
               <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
               <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
               <button type="submit" class="btn btn-warning">Unportalize</button>