Browse Source

Merge pull request #821 from weseek/master

release v3.3.7
Yuki Takei 7 years ago
parent
commit
a3552d376e
35 changed files with 768 additions and 737 deletions
  1. 19 4
      CHANGES.md
  2. 4 4
      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. 123 1
      src/client/js/components/PageEditor/CodeMirrorEditor.js
  11. 1 1
      src/client/js/components/PageEditor/Editor.jsx
  12. 0 169
      src/client/js/components/PageListSearch.js
  13. 112 0
      src/client/js/components/SearchForm.js
  14. 7 3
      src/client/js/components/SearchPage.js
  15. 0 61
      src/client/js/components/SearchPage/SearchForm.js
  16. 61 0
      src/client/js/components/SearchPage/SearchPageForm.js
  17. 5 12
      src/client/js/components/SearchTypeahead.js
  18. 4 4
      src/client/js/legacy/crowi.js
  19. 3 0
      src/client/styles/agile-admin/inverse/colors/_apply-colors-light.scss
  20. 3 4
      src/client/styles/scss/_comment_growi.scss
  21. 1 1
      src/client/styles/scss/_create-page.scss
  22. 20 0
      src/client/styles/scss/_editor-navbar.scss
  23. 0 11
      src/client/styles/scss/_mixins.scss
  24. 24 27
      src/client/styles/scss/_search.scss
  25. 1 0
      src/client/styles/scss/style.scss
  26. 4 4
      src/server/routes/page.js
  27. 2 8
      src/server/routes/search.js
  28. 73 77
      src/server/util/search.js
  29. 1 2
      src/server/util/swigFunctions.js
  30. 0 14
      src/server/views/layout-crowi/page_list.html
  31. 1 1
      src/server/views/modal/create_page.html
  32. 5 1
      src/server/views/modal/duplicate.html
  33. 5 1
      src/server/views/modal/rename.html
  34. 1 1
      src/server/views/modal/unportalize.html
  35. 119 108
      yarn.lock

+ 19 - 4
CHANGES.md

@@ -1,14 +1,27 @@
 CHANGES
 ========
 
-## 3.3.5-RC
+## 3.3.7-RC
+
+* 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.6
 
 * Improvement: Site URL settings must be set
 * Improvement: Site URL settings can be set with environment variable
 * Fix: "Anyone with the link" ACL doesn't work correctly
-    * Introduced 3.3.0
+    * Introduced by 3.3.0
 * Fix: Related pages list of /admin/user-group-detail/xxx doesn't show anything
-    * Introduced 3.3.0
+    * Introduced by 3.3.0
 * Fix: Diff of revision contents doesn't appeared when notifing with slack
 * Fix: NPE occured on /admin/security when Crowi Classic Auth Mechanism is set
 * Fix: Coudn't render Timing Diagram with PlantUML
@@ -23,6 +36,8 @@ CHANGES
     * sinon
     * sinon-chai
 
+## 3.3.5 (Missing number)
+
 ## 3.3.4
 
 * Improvement: SAML configuration with environment variables
@@ -50,7 +65,7 @@ CHANGES
 ## 3.3.2
 
 * Fix: Specified Group ACL is not persisted correctly
-    * Introduced 3.3.0
+    * Introduced by 3.3.0
 
 ## 3.3.1
 

+ 4 - 4
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.3.6-RC",
+  "version": "3.3.7-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -173,7 +173,7 @@
     "mocha": "^5.2.0",
     "morgan": "^1.9.0",
     "node-dev": "^3.1.3",
-    "node-sass": "^4.5.0",
+    "node-sass": "^4.11.0",
     "nodelist-foreach-polyfill": "^1.2.0",
     "normalize-path": "^3.0.0",
     "null-loader": "^0.1.1",
@@ -218,9 +218,9 @@
     "debug": "src/lib/service/logger/alias-for-debug"
   },
   "engines": {
-    "node": ">=8.11.1 <9",
+    "node": ">=8.11.1 <11",
     "npm": ">=5.6.0 <7",
-    "yarn": "^1.5.1"
+    "yarn": ">=1.5.1 <2"
   },
   "config": {
     "blanket": {

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

+ 123 - 1
src/client/js/components/PageEditor/CodeMirrorEditor.js

@@ -95,6 +95,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
     this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this);
 
+    this.makeHeaderHandler = this.makeHeaderHandler.bind(this);
     this.showHandsonTableHandler = this.showHandsonTableHandler.bind(this);
   }
 
@@ -547,12 +548,133 @@ export default class CodeMirrorEditor extends AbstractEditor {
     );
   }
 
+  /**
+   * return a function to replace a selected range with prefix + selection + suffix
+   *
+   * The cursor after replacing is inserted between the selection and the suffix.
+   */
+  createReplaceSelectionHandler(prefix, suffix) {
+    return () => {
+      const cm = this.getCodeMirror();
+      const selection = cm.getDoc().getSelection();
+      const curStartPos = cm.getCursor('from');
+      const curEndPos = cm.getCursor('to');
+
+      const curPosAfterReplacing = {};
+      curPosAfterReplacing.line = curEndPos.line;
+      if (curStartPos.line === curEndPos.line) {
+        curPosAfterReplacing.ch = curEndPos.ch + prefix.length;
+      }
+      else {
+        curPosAfterReplacing.ch = curEndPos.ch;
+      }
+
+      cm.getDoc().replaceSelection(prefix + selection + suffix);
+      cm.setCursor(curPosAfterReplacing);
+      cm.focus();
+    };
+  }
+
+  /**
+   * return a function to add prefix to selected each lines
+   *
+   * The cursor after editing is inserted between the end of the selection.
+   */
+  createAddPrefixToEachLinesHandler(prefix) {
+    return () => {
+      const cm = this.getCodeMirror();
+      const startLineNum = cm.getCursor('from').line;
+      const endLineNum = cm.getCursor('to').line;
+
+      const lines = [];
+      for (let i = startLineNum; i <= endLineNum; i++) {
+        lines.push(prefix + cm.getDoc().getLine(i));
+      }
+      const replacement = lines.join('\n') + '\n';
+      cm.getDoc().replaceRange(replacement, {line: startLineNum, ch: 0}, {line: endLineNum + 1, ch: 0});
+
+      cm.setCursor(endLineNum, cm.getDoc().getLine(endLineNum).length);
+      cm.focus();
+    };
+  }
+
+  /**
+   * make a selected line a header
+   *
+   * The cursor after editing is inserted between the end of the line.
+   */
+  makeHeaderHandler() {
+    const cm = this.getCodeMirror();
+    const lineNum = cm.getCursor('from').line;
+    const line = cm.getDoc().getLine(lineNum);
+    let prefix = '#';
+    if (!line.startsWith('#')) {
+      prefix += ' ';
+    }
+    cm.getDoc().replaceRange(prefix, {line: lineNum, ch: 0}, {line: lineNum, ch: 0});
+    cm.focus();
+  }
+
   showHandsonTableHandler() {
     this.refs.handsontableModal.show(mtu.getMarkdownTable(this.getCodeMirror()));
   }
 
   getNavbarItems() {
-    return <Button bsSize="small" onClick={ this.showHandsonTableHandler }><img src="/images/icons/editor/table.svg" width="14" /></Button>;
+    // The following styles will be removed after creating icons for the editor navigation bar.
+    const paddingTopBottom54 = {'paddingTop': '6px', 'paddingBottom': '5px'};
+    const paddingBottom6 = {'paddingBottom': '7px'};
+    const fontSize18 = {'fontSize': '18px'};
+
+    return [
+      <Button key='nav-item-bold' bsSize="small" title={'Bold'}
+              onClick={ this.createReplaceSelectionHandler('**', '**') }>
+        <i className={'fa fa-bold'}></i>
+      </Button>,
+      <Button key='nav-item-italic' bsSize="small" title={'Italic'}
+              onClick={ this.createReplaceSelectionHandler('*', '*') }>
+        <i className={'fa fa-italic'}></i>
+      </Button>,
+      <Button key='nav-item-strikethough' bsSize="small" title={'Strikethrough'}
+              onClick={ this.createReplaceSelectionHandler('~~', '~~') }>
+        <i className={'fa fa-strikethrough'}></i>
+      </Button>,
+      <Button key='nav-item-header' bsSize="small" title={'Heading'}
+              onClick={ this.makeHeaderHandler }>
+        <i className={'fa fa-header'}></i>
+      </Button>,
+      <Button key='nav-item-code' bsSize="small" title={'Inline Code'}
+              onClick={ this.createReplaceSelectionHandler('`', '`') }>
+        <i className={'fa fa-code'}></i>
+      </Button>,
+      <Button key='nav-item-quote' bsSize="small" title={'Quote'}
+              onClick={ this.createAddPrefixToEachLinesHandler('> ') } style={paddingBottom6}>
+        <i className={'ti-quote-right'}></i>
+      </Button>,
+      <Button key='nav-item-ul' bsSize="small" title={'List'}
+              onClick={ this.createAddPrefixToEachLinesHandler('- ') } style={paddingTopBottom54}>
+        <i className={'ti-list'} style={fontSize18}></i>
+      </Button>,
+      <Button key='nav-item-ol' bsSize="small" title={'Numbered List'}
+              onClick={ this.createAddPrefixToEachLinesHandler('1. ') } style={paddingTopBottom54}>
+        <i className={'ti-list-ol'} style={fontSize18}></i>
+      </Button>,
+      <Button key='nav-item-checkbox' bsSize="small" title={'Check List'}
+              onClick={ this.createAddPrefixToEachLinesHandler('- [ ] ') } style={paddingBottom6}>
+        <i className={'ti-check-box'}></i>
+      </Button>,
+      <Button key='nav-item-link' bsSize="small" title={'Link'}
+              onClick={ this.createReplaceSelectionHandler('[', ']()') } style={paddingBottom6}>
+        <i className={'icon-link'}></i>
+      </Button>,
+      <Button key='nav-item-image' bsSize="small" title={'Image'}
+              onClick={ this.createReplaceSelectionHandler('![', ']()') } style={paddingBottom6}>
+        <i className={'icon-picture'}></i>
+      </Button>,
+      <Button key='nav-item-table' bsSize="small" title={'Table'}
+              onClick={ this.showHandsonTableHandler }>
+        <img src="/images/icons/editor/table.svg" width="14" height="14" />
+      </Button>
+    ];
   }
 
   render() {

+ 1 - 1
src/client/js/components/PageEditor/Editor.jsx

@@ -210,7 +210,7 @@ export default class Editor extends AbstractEditor {
   renderNavbar() {
     return (
       <div className="m-0 navbar navbar-default navbar-editor" style={{ minHeight: 'unset' }}>
-        <ul className="pr-4 nav nav-navbar navbar-right">
+        <ul className="pl-2 nav nav-navbar">
           { this.getNavbarItems() != null && this.getNavbarItems().map((item, idx) => {
             return <li key={idx}>{item}</li>;
           }) }

+ 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 - 11
src/client/styles/scss/_mixins.scss

@@ -50,17 +50,6 @@
           height: calc(100vh - #{$header-plus-footer});
         }
 
-        .navbar-editor {
-          button {
-            padding: 7px 8px;
-            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

+ 73 - 77
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) {
@@ -541,14 +532,20 @@ SearchClient.prototype.filterPagesByViewer = async function(query, user, userGro
 
   const grantConditions = [
     { term: { grant: GRANT_PUBLIC } },
-    { bool: {
-      must: [
-        { term: { grant: GRANT_RESTRICTED } },
-        { term: { granted_users: user._id.toString() } }
-      ]
-    } },
   ];
 
+  // ensure to hit to GRANT_RESTRICTED pages that the user specified at own
+  if (user != null) {
+    grantConditions.push(
+      { bool: {
+        must: [
+          { term: { grant: GRANT_RESTRICTED } },
+          { term: { granted_users: user._id.toString() } }
+        ]
+      } }
+    );
+  }
+
   if (showPagesRestrictedByOwner) {
     grantConditions.push(
       { term: { grant: GRANT_SPECIFIED } },
@@ -645,34 +642,12 @@ SearchClient.prototype.appendFunctionScore = function(query) {
   };
 };
 
-SearchClient.prototype.searchKeyword = async function(keyword, 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.filterPagesByType(query, type);
-  await this.filterPagesByViewer(query, user, userGroups);
-
-  this.appendResultSize(query, from, size);
-
-  this.appendFunctionScore(query);
-
-  return this.search(query);
-};
-
-SearchClient.prototype.searchByPath = async function(keyword, prefix) {
-  // TODO path 名だけから検索
-};
-
-SearchClient.prototype.searchKeywordUnderPath = async function(keyword, path, 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.appendCriteriaForPathFilter(query, path);
+  this.appendCriteriaForQueryString(query, queryString);
 
   this.filterPagesByType(query, type);
   await this.filterPagesByViewer(query, user, userGroups);
@@ -684,21 +659,23 @@ SearchClient.prototype.searchKeywordUnderPath = async function(keyword, path, us
   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();
@@ -712,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]);
+      }
     }
   });
 
@@ -730,6 +724,8 @@ SearchClient.prototype.getParsedKeywords = function(keyword) {
     not_match: notMatchWords,
     phrase: phraseWords,
     not_phrase: notPhraseWords,
+    prefix: prefixPaths,
+    not_prefix: notPrefixPaths,
   };
 };
 

+ 1 - 2
src/server/util/swigFunctions.js

@@ -136,8 +136,7 @@ module.exports = function(crowi, app, req, locals) {
   };
 
   locals.passportSamlLoginEnabled = function() {
-    let config = crowi.getConfig();
-    return locals.isEnabledPassport() && config.crowi['security:passport-saml:isEnabled'];
+    return locals.isEnabledPassport() && locals.getConfig('crowi', 'security:passport-saml:isEnabled');
   };
 
   locals.getSamlMissingMandatoryConfigKeys = function() {

+ 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>

+ 119 - 108
yarn.lock

@@ -358,6 +358,16 @@ ajv@^6.1.0:
     fast-json-stable-stringify "^2.0.0"
     json-schema-traverse "^0.3.0"
 
+ajv@^6.5.5:
+  version "6.7.0"
+  resolved "https://registry.yarnpkg.com/ajv/-/ajv-6.7.0.tgz#e3ce7bb372d6577bb1839f1dfdfcbf5ad2948d96"
+  integrity sha512-RZXPviBTtfmtka9n9sy1N5M5b82CbxWIR6HIis4s3WQTXDJamc/0gpCWNGz6EWdWp4DOfjzJfhz/AS9zVPjjWg==
+  dependencies:
+    fast-deep-equal "^2.0.1"
+    fast-json-stable-stringify "^2.0.0"
+    json-schema-traverse "^0.4.1"
+    uri-js "^4.2.2"
+
 align-text@^0.1.1, align-text@^0.1.3:
   version "0.1.4"
   resolved "https://registry.yarnpkg.com/align-text/-/align-text-0.1.4.tgz#0cd90a561093f35d0a99256c22b7069433fad117"
@@ -669,6 +679,11 @@ aws4@^1.2.1, aws4@^1.6.0:
   version "1.6.0"
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e"
 
+aws4@^1.8.0:
+  version "1.8.0"
+  resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.8.0.tgz#f0e003d9ca9e7f59c7a508945d7b2ef9a04a542f"
+  integrity sha512-ReZxvNHIOv88FlT7rxcXIIC0fPt4KZqZbOlivyWtXLt8ESx84zd3kMC6iK5jVeS2qt+g7ftS7ye4fi06X5rtRQ==
+
 axios@^0.18.0:
   version "0.18.0"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"
@@ -1762,10 +1777,6 @@ cardinal@^1.0.0:
     ansicolors "~0.2.1"
     redeyed "~1.0.0"
 
-caseless@~0.11.0:
-  version "0.11.0"
-  resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.11.0.tgz#715b96ea9841593cc33067923f5ec60ebda4f7d7"
-
 caseless@~0.12.0:
   version "0.12.0"
   resolved "https://registry.yarnpkg.com/caseless/-/caseless-0.12.0.tgz#1b681c21ff84033c826543090689420d187151dc"
@@ -2078,6 +2089,13 @@ combined-stream@^1.0.5, combined-stream@~1.0.5:
   dependencies:
     delayed-stream "~1.0.0"
 
+combined-stream@^1.0.6, combined-stream@~1.0.6:
+  version "1.0.7"
+  resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.7.tgz#2d1d24317afb8abe95d6d2c0b07b57813539d828"
+  integrity sha512-brWl9y6vOB1xYPZcpZde3N9zDByXTosAeMDo4p1wzo6UMOX4vumB+TP1RZ76sfE6Md68Q0NJSrE/gbezd4Ul+w==
+  dependencies:
+    delayed-stream "~1.0.0"
+
 commander@2.15.1, commander@^2.2.0:
   version "2.15.1"
   resolved "https://registry.yarnpkg.com/commander/-/commander-2.15.1.tgz#df46e867d0fc2aec66a34662b406a9ccafff5b0f"
@@ -3429,6 +3447,11 @@ extend@^3.0.0, extend@^3.0.1, extend@~3.0.0, extend@~3.0.1:
   version "3.0.1"
   resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.1.tgz#a755ea7bc1adfcc5a31ce7e762dbaadc5e636444"
 
+extend@~3.0.2:
+  version "3.0.2"
+  resolved "https://registry.yarnpkg.com/extend/-/extend-3.0.2.tgz#f8b1136b4071fbd8eb140aff858b1019ec2915fa"
+  integrity sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==
+
 external-editor@^2.1.0:
   version "2.2.0"
   resolved "https://registry.yarnpkg.com/external-editor/-/external-editor-2.2.0.tgz#045511cfd8d133f3846673d1047c154e214ad3d5"
@@ -3725,6 +3748,15 @@ form-data@~2.3.1:
     combined-stream "^1.0.5"
     mime-types "^2.1.12"
 
+form-data@~2.3.2:
+  version "2.3.3"
+  resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.3.tgz#dcce52c05f644f298c6a7ab936bd724ceffbf3a6"
+  integrity sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==
+  dependencies:
+    asynckit "^0.4.0"
+    combined-stream "^1.0.6"
+    mime-types "^2.1.12"
+
 formidable@~1.0.14:
   version "1.0.17"
   resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.0.17.tgz#ef5491490f9433b705faa77249c99029ae348559"
@@ -3863,16 +3895,6 @@ gcp-metadata@^0.7.0:
     extend "^3.0.1"
     retry-axios "0.3.2"
 
-generate-function@^2.0.0:
-  version "2.0.0"
-  resolved "https://registry.yarnpkg.com/generate-function/-/generate-function-2.0.0.tgz#6858fe7c0969b7d4e9093337647ac79f60dfbe74"
-
-generate-object-property@^1.1.0:
-  version "1.2.0"
-  resolved "https://registry.yarnpkg.com/generate-object-property/-/generate-object-property-1.2.0.tgz#9c0e1c40308ce804f4783618b937fa88f99d50d0"
-  dependencies:
-    is-property "^1.0.0"
-
 get-caller-file@^1.0.1:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5"
@@ -4103,15 +4125,6 @@ har-schema@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-2.0.0.tgz#a94c2224ebcac04782a0d9035521f24735b7ec92"
 
-har-validator@~2.0.6:
-  version "2.0.6"
-  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-2.0.6.tgz#cdcbc08188265ad119b6a5a7c8ab70eecfb5d27d"
-  dependencies:
-    chalk "^1.1.1"
-    commander "^2.9.0"
-    is-my-json-valid "^2.12.4"
-    pinkie-promise "^2.0.0"
-
 har-validator@~4.2.1:
   version "4.2.1"
   resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-4.2.1.tgz#33481d0f1bbff600dd203d75812a6a5fba002e2a"
@@ -4126,6 +4139,14 @@ har-validator@~5.0.3:
     ajv "^5.1.0"
     har-schema "^2.0.0"
 
+har-validator@~5.1.0:
+  version "5.1.3"
+  resolved "https://registry.yarnpkg.com/har-validator/-/har-validator-5.1.3.tgz#1ef89ebd3e4996557675eed9893110dc350fa080"
+  integrity sha512-sNvOCzEQNr/qrvJgc3UG/kD4QtlHycrzwS+6mfTrrSq97BvaYcPZZI1ZSqGSPR73Cxn4LKTD4PttRwfU7jWq5g==
+  dependencies:
+    ajv "^6.5.5"
+    har-schema "^2.0.0"
+
 has-ansi@^2.0.0:
   version "2.0.0"
   resolved "https://registry.yarnpkg.com/has-ansi/-/has-ansi-2.0.0.tgz#34f5049ce1ecdf2b0649af3ef24e45ed35416d91"
@@ -4756,15 +4777,6 @@ is-glob@^4.0.0:
   dependencies:
     is-extglob "^2.1.1"
 
-is-my-json-valid@^2.12.4:
-  version "2.17.1"
-  resolved "https://registry.yarnpkg.com/is-my-json-valid/-/is-my-json-valid-2.17.1.tgz#3da98914a70a22f0a8563ef1511a246c6fc55471"
-  dependencies:
-    generate-function "^2.0.0"
-    generate-object-property "^1.1.0"
-    jsonpointer "^4.0.0"
-    xtend "^4.0.0"
-
 is-number-like@^1.0.3:
   version "1.0.8"
   resolved "https://registry.yarnpkg.com/is-number-like/-/is-number-like-1.0.8.tgz#2e129620b50891042e44e9bbbb30593e75cfbbe3"
@@ -4835,10 +4847,6 @@ is-promise@^2.1.0:
   version "2.1.0"
   resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa"
 
-is-property@^1.0.0:
-  version "1.0.2"
-  resolved "https://registry.yarnpkg.com/is-property/-/is-property-1.0.2.tgz#57fe1c4e48474edd65b09911f26b1cd4095dda84"
-
 is-regex@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.0.4.tgz#5517489b547091b0930e095654ced25ee97e9491"
@@ -5047,10 +5055,6 @@ jsonify@~0.0.0:
   version "0.0.0"
   resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
 
-jsonpointer@^4.0.0:
-  version "4.0.1"
-  resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-4.0.1.tgz#4fd92cb34e0e9db3c89c8622ecf51f9b978c6cb9"
-
 jsprim@^1.2.2:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jsprim/-/jsprim-1.4.1.tgz#313e66bc1e5cc06e438bc1b7499c2e5c56acb6a2"
@@ -5713,6 +5717,11 @@ mime-db@~1.33.0:
   version "1.33.0"
   resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db"
 
+mime-db@~1.37.0:
+  version "1.37.0"
+  resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.37.0.tgz#0b6a0ce6fdbe9576e25f1f2d2fde8830dc0ad0d8"
+  integrity sha512-R3C4db6bgQhlIhPU48fUtdVmKnflq+hRdad7IyKhtFj06VPNVdk2RhiYL3UjQIlso8L+YxAtFkobT0VK+S/ybg==
+
 mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7:
   version "2.1.17"
   resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a"
@@ -5725,6 +5734,13 @@ mime-types@^2.1.3, mime-types@~2.1.18:
   dependencies:
     mime-db "~1.33.0"
 
+mime-types@~2.1.19:
+  version "2.1.21"
+  resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.21.tgz#28995aa1ecb770742fe6ae7e58f9181c744b3f96"
+  integrity sha512-3iL6DbwpyLzjR3xHSFNFeb9Nz/M8WDkX33t1GFQnFOllWk8pOrh/LSrB5OXlnlW5P9LH73X6loW/eogc+F5lJg==
+  dependencies:
+    mime-db "~1.37.0"
+
 mime@1.3.4:
   version "1.3.4"
   resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
@@ -6025,7 +6041,12 @@ mv@~2:
     ncp "~2.0.0"
     rimraf "~2.4.0"
 
-nan@^2.3.0, nan@^2.3.2, nan@^2.3.3:
+nan@^2.10.0:
+  version "2.12.1"
+  resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
+  integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
+
+nan@^2.3.0, nan@^2.3.3:
   version "2.8.0"
   resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a"
 
@@ -6125,19 +6146,19 @@ node-forge@^0.7.1:
   version "0.7.1"
   resolved "https://registry.yarnpkg.com/node-forge/-/node-forge-0.7.1.tgz#9da611ea08982f4b94206b3beb4cc9665f20c300"
 
-node-gyp@^3.3.1:
-  version "3.6.2"
-  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.6.2.tgz#9bfbe54562286284838e750eac05295853fa1c60"
+node-gyp@^3.8.0:
+  version "3.8.0"
+  resolved "https://registry.yarnpkg.com/node-gyp/-/node-gyp-3.8.0.tgz#540304261c330e80d0d5edce253a68cb3964218c"
+  integrity sha512-3g8lYefrRRzvGeSowdJKAKyks8oUpLEd/DyPV4eMhVlhJ0aNaZqIrNUIPuEWWTAoPqyFkfGrM67MC69baqn6vA==
   dependencies:
     fstream "^1.0.0"
     glob "^7.0.3"
     graceful-fs "^4.1.2"
-    minimatch "^3.0.2"
     mkdirp "^0.5.0"
     nopt "2 || 3"
     npmlog "0 || 1 || 2 || 3 || 4"
     osenv "0"
-    request "2"
+    request "^2.87.0"
     rimraf "2"
     semver "~5.3.0"
     tar "^2.0.0"
@@ -6220,9 +6241,10 @@ node-releases@^1.0.0-alpha.10:
   dependencies:
     semver "^5.3.0"
 
-node-sass@^4.5.0:
-  version "4.7.2"
-  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.7.2.tgz#9366778ba1469eb01438a9e8592f4262bcb6794e"
+node-sass@^4.11.0:
+  version "4.11.0"
+  resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a"
+  integrity sha512-bHUdHTphgQJZaF1LASx0kAviPH7sGlcyNhWade4eVIpFp6tsn7SV8xNMTbsQFpEV9VXpnwTTnNYlfsZXgGgmkA==
   dependencies:
     async-foreach "^0.1.3"
     chalk "^1.1.1"
@@ -6236,10 +6258,10 @@ node-sass@^4.5.0:
     lodash.mergewith "^4.6.0"
     meow "^3.7.0"
     mkdirp "^0.5.1"
-    nan "^2.3.2"
-    node-gyp "^3.3.1"
+    nan "^2.10.0"
+    node-gyp "^3.8.0"
     npmlog "^4.0.0"
-    request "~2.79.0"
+    request "^2.88.0"
     sass-graph "^2.2.4"
     stdout-stream "^1.4.0"
     "true-case-path" "^1.0.2"
@@ -6373,6 +6395,11 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2:
   version "0.8.2"
   resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43"
 
+oauth-sign@~0.9.0:
+  version "0.9.0"
+  resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.9.0.tgz#47a7b016baa68b5fa0ecf3dee08a85c679ac6455"
+  integrity sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==
+
 oauth@0.9.x:
   version "0.9.15"
   resolved "https://registry.yarnpkg.com/oauth/-/oauth-0.9.15.tgz#bd1fefaf686c96b75475aed5196412ff60cfb9c1"
@@ -7328,6 +7355,11 @@ pseudomap@^1.0.2:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/pseudomap/-/pseudomap-1.0.2.tgz#f052a28da70e618917ef0a8ac34c1ae5a68286b3"
 
+psl@^1.1.24:
+  version "1.1.31"
+  resolved "https://registry.yarnpkg.com/psl/-/psl-1.1.31.tgz#e9aa86d0101b5b105cbe93ac6b784cd547276184"
+  integrity sha512-/6pt4+C+T+wZUieKR620OpzN/LlnNKuWjy1iFLQ/UG35JqHlR/89MP1d96dUfkf6Dne3TuLQzOYEYshJ+Hx8mw==
+
 public-encrypt@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/public-encrypt/-/public-encrypt-4.0.0.tgz#39f699f3a46560dd5ebacbca693caf7c65c18cc6"
@@ -7388,14 +7420,10 @@ qs@6.5.1, qs@~6.5.1:
   version "6.5.1"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"
 
-qs@^6.5.2:
+qs@^6.5.2, qs@~6.5.2:
   version "6.5.2"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.2.tgz#cb3ae806e8740444584ef154ce8ee98d403f3e36"
 
-qs@~6.3.0:
-  version "6.3.2"
-  resolved "https://registry.yarnpkg.com/qs/-/qs-6.3.2.tgz#e75bd5f6e268122a2a0e0bda630b2550c166502c"
-
 qs@~6.4.0:
   version "6.4.0"
   resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
@@ -7814,33 +7842,6 @@ replacestream@^4.0.3:
     object-assign "^4.0.1"
     readable-stream "^2.0.2"
 
-request@2:
-  version "2.83.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.83.0.tgz#ca0b65da02ed62935887808e6f510381034e3356"
-  dependencies:
-    aws-sign2 "~0.7.0"
-    aws4 "^1.6.0"
-    caseless "~0.12.0"
-    combined-stream "~1.0.5"
-    extend "~3.0.1"
-    forever-agent "~0.6.1"
-    form-data "~2.3.1"
-    har-validator "~5.0.3"
-    hawk "~6.0.2"
-    http-signature "~1.2.0"
-    is-typedarray "~1.0.0"
-    isstream "~0.1.2"
-    json-stringify-safe "~5.0.1"
-    mime-types "~2.1.17"
-    oauth-sign "~0.8.2"
-    performance-now "^2.1.0"
-    qs "~6.5.1"
-    safe-buffer "^5.1.1"
-    stringstream "~0.0.5"
-    tough-cookie "~2.3.3"
-    tunnel-agent "^0.6.0"
-    uuid "^3.1.0"
-
 request@2.81.0:
   version "2.81.0"
   resolved "https://registry.yarnpkg.com/request/-/request-2.81.0.tgz#c6928946a0e06c5f8d6f8a9333469ffda46298a0"
@@ -7920,30 +7921,31 @@ request@^2.74.0:
     tunnel-agent "^0.6.0"
     uuid "^3.1.0"
 
-request@~2.79.0:
-  version "2.79.0"
-  resolved "https://registry.yarnpkg.com/request/-/request-2.79.0.tgz#4dfe5bf6be8b8cdc37fcf93e04b65577722710de"
+request@^2.87.0, request@^2.88.0:
+  version "2.88.0"
+  resolved "https://registry.yarnpkg.com/request/-/request-2.88.0.tgz#9c2fca4f7d35b592efe57c7f0a55e81052124fef"
+  integrity sha512-NAqBSrijGLZdM0WZNsInLJpkJokL72XYjUpnB0iwsRgxh7dB6COrHnTBNwN0E+lHDAJzu7kLAkDeY08z2/A0hg==
   dependencies:
-    aws-sign2 "~0.6.0"
-    aws4 "^1.2.1"
-    caseless "~0.11.0"
-    combined-stream "~1.0.5"
-    extend "~3.0.0"
+    aws-sign2 "~0.7.0"
+    aws4 "^1.8.0"
+    caseless "~0.12.0"
+    combined-stream "~1.0.6"
+    extend "~3.0.2"
     forever-agent "~0.6.1"
-    form-data "~2.1.1"
-    har-validator "~2.0.6"
-    hawk "~3.1.3"
-    http-signature "~1.1.0"
+    form-data "~2.3.2"
+    har-validator "~5.1.0"
+    http-signature "~1.2.0"
     is-typedarray "~1.0.0"
     isstream "~0.1.2"
     json-stringify-safe "~5.0.1"
-    mime-types "~2.1.7"
-    oauth-sign "~0.8.1"
-    qs "~6.3.0"
-    stringstream "~0.0.4"
-    tough-cookie "~2.3.0"
-    tunnel-agent "~0.4.1"
-    uuid "^3.0.0"
+    mime-types "~2.1.19"
+    oauth-sign "~0.9.0"
+    performance-now "^2.1.0"
+    qs "~6.5.2"
+    safe-buffer "^5.1.2"
+    tough-cookie "~2.4.3"
+    tunnel-agent "^0.6.0"
+    uuid "^3.3.2"
 
 requestretry@^1.2.2:
   version "1.13.0"
@@ -9071,6 +9073,14 @@ tough-cookie@~2.3.0, tough-cookie@~2.3.3:
   dependencies:
     punycode "^1.4.1"
 
+tough-cookie@~2.4.3:
+  version "2.4.3"
+  resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.4.3.tgz#53f36da3f47783b0925afa06ff9f3b165280f781"
+  integrity sha512-Q5srk/4vDM54WJsJio3XNn6K2sCG+CQ8G5Wz6bZhRZoAe/+TxjWB/GlFAnYEbkYVlON9FMk/fE3h2RLpPXo4lQ==
+  dependencies:
+    psl "^1.1.24"
+    punycode "^1.4.1"
+
 trim-newlines@^1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613"
@@ -9107,10 +9117,6 @@ tunnel-agent@^0.6.0:
   dependencies:
     safe-buffer "^5.0.1"
 
-tunnel-agent@~0.4.1:
-  version "0.4.3"
-  resolved "https://registry.yarnpkg.com/tunnel-agent/-/tunnel-agent-0.4.3.tgz#6373db76909fe570e08d73583365ed828a74eeeb"
-
 tweetnacl@^0.14.3, tweetnacl@~0.14.0:
   version "0.14.5"
   resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64"
@@ -9310,7 +9316,7 @@ upath@^1.0.5:
   version "1.1.0"
   resolved "https://registry.yarnpkg.com/upath/-/upath-1.1.0.tgz#35256597e46a581db4793d0ce47fa9aebfc9fabd"
 
-uri-js@^4.2.1:
+uri-js@^4.2.1, uri-js@^4.2.2:
   version "4.2.2"
   resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"
   dependencies:
@@ -9398,6 +9404,11 @@ uuid@^3.2.1:
   version "3.2.1"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14"
 
+uuid@^3.3.2:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+  integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==
+
 uws@~0.14.4:
   version "0.14.5"
   resolved "https://registry.yarnpkg.com/uws/-/uws-0.14.5.tgz#67aaf33c46b2a587a5f6666d00f7691328f149dc"