Bladeren bron

Merge pull request #574 from weseek/master

release v3.1.14
Yuki Takei 8 jaren geleden
bovenliggende
commit
cd2a37e07f

+ 8 - 2
CHANGES.md

@@ -3,10 +3,16 @@ CHANGES
 
 ## 3.2.0-RC
 
-* Feature: Simultaneously edit by multiple people with HackMD integration
+* Feature: HackMD integration so that user can simultaneously edit with multiple people
 
+## 3.1.14-RC
 
-## 3.1.13-RC
+* Improvement: Show help for header search box
+* Improvement: Add Markdown Cheatsheet to Editor component
+* Fix: Couldn't delete page completely from search result page
+* Fix: Tabs of trash page are broken
+
+## 3.1.13
 
 * Feature: Global Notification
 * Feature: Send Global Notification with E-mail

+ 16 - 1
lib/models/page.js

@@ -410,7 +410,7 @@ module.exports = function(crowi) {
 
   pageSchema.statics.isCreatableName = function(name) {
     var forbiddenPages = [
-      /\^|\$|\*|\+|#/,
+      /\^|\$|\*|\+|#|%/,
       /^\/-\/.*/,
       /^\/_r\/.*/,
       /^\/_apix?(\/.*)?/,
@@ -1059,8 +1059,14 @@ module.exports = function(crowi) {
   pageSchema.statics.deletePage = function(pageData, user, options) {
     var Page = this
       , newPath = Page.getDeletedPageName(pageData.path)
+      , isTrashed = checkIfTrashed(pageData.path)
       ;
+
     if (Page.isDeletableName(pageData.path)) {
+      if (isTrashed) {
+        return Page.completelyDeletePage(pageData, user, options);
+      }
+
       return Page.rename(pageData, newPath, user, {createRedirectPage: true})
         .then((updatedPageData) => {
           return Page.updatePageProperty(updatedPageData, {status: STATUS_DELETED, lastUpdateUser: user});
@@ -1074,12 +1080,21 @@ module.exports = function(crowi) {
     }
   };
 
+  const checkIfTrashed = (path) => {
+    return (path.search(/^\/trash/) !== -1);
+  };
+
   pageSchema.statics.deletePageRecursively = function(pageData, user, options) {
     var Page = this
       , path = pageData.path
       , options = options || {}
+      , isTrashed = checkIfTrashed(pageData.path);
       ;
 
+    if (isTrashed) {
+      return Page.completelyDeletePageRecursively(pageData, user, options);
+    }
+
     return Page.generateQueryToListWithDescendants(path, user, options)
       .then(function(pages) {
         return Promise.all(pages.map(function(page) {

+ 1 - 1
lib/models/revision.js

@@ -12,7 +12,7 @@ module.exports = function(crowi) {
     body: { type: String, required: true, get: (data) => {
       // replace CR/CRLF to LF above v3.1.5
       // see https://github.com/weseek/growi/issues/463
-      return data.replace(/\r\n?/g, '\n');
+      return data ? data.replace(/\r\n?/g, '\n') : '';
     }},
     format: { type: String, default: 'markdown' },
     author: { type: ObjectId, ref: 'User' },

+ 2 - 2
lib/routes/page.js

@@ -1081,9 +1081,9 @@ module.exports = function(crowi, app) {
     var previousRevision = req.body.revision_id || null;
 
     // get completely flag
-    const isCompletely = (req.body.completely !== undefined);
+    const isCompletely = (req.body.completely != null);
     // get recursively flag
-    const isRecursively = (req.body.recursively !== undefined);
+    const isRecursively = (req.body.recursively != null);
 
     Page.findPageByIdAndGrantedUser(pageId, req.user)
       .then(function(pageData) {

+ 3 - 3
lib/util/slack.js

@@ -79,7 +79,7 @@ module.exports = function(crowi) {
       var value = line.value.replace(/\r\n|\r/g, '\n');
       /* eslint-enable */
       if (line.added) {
-        diffText += `:pencil2: ...\n${line.value}`;
+        diffText += `${line.value} ... :lower_left_fountain_pen:`;
       }
       else if (line.removed) {
         // diffText += '-' + line.value.replace(/(.+)?\n/g, '- $1\n');
@@ -179,10 +179,10 @@ module.exports = function(crowi) {
 
     const pageUrl = `<${url}${path}|${path}>`;
     if (updateType == 'create') {
-      text = `:white_check_mark: ${user.username} created a new page! ${pageUrl}`;
+      text = `:rocket: ${user.username} created a new page! ${pageUrl}`;
     }
     else {
-      text = `:up: ${user.username} updated ${pageUrl}`;
+      text = `:heavy_check_mark: ${user.username} updated ${pageUrl}`;
     }
 
     return text;

+ 17 - 17
lib/views/admin/global-notification-detail.html

@@ -87,46 +87,46 @@
             <fieldset class="col-sm-offset-1 col-sm-5">
               <div class="form-group">
                 <h3>{{ t('notification_setting.trigger_events') }}</h3>
-                <div class="checkbox checkbox-info">
+                <div class="checkbox checkbox-inverse">
                   <input type="checkbox" id="trigger-event-pageCreate" name="notificationGlobal[triggerEvent:pageCreate]" value="pageCreate"
                     {% if setting && (setting.triggerEvents.indexOf('pageCreate') != -1) %}checked{% endif %} />
                   <label for="trigger-event-pageCreate">
-                    <span class="label label-info"><i class="icon-doc"></i> CREATE</span> - {{ t('notification_setting.event_pageCreate') }}
+                    <span class="label label-success"><i class="icon-doc"></i> CREATE</span> - {{ t('notification_setting.event_pageCreate') }}
                   </label>
                 </div>
-                <div class="checkbox checkbox-info">
+                <div class="checkbox checkbox-inverse">
                   <input type="checkbox" id="trigger-event-pageEdit" name="notificationGlobal[triggerEvent:pageEdit]" value="pageEdit"
                     {% if setting && (setting.triggerEvents.indexOf('pageEdit') != -1) %}checked{% endif %} />
                   <label for="trigger-event-pageEdit">
-                    <span class="label label-info"><i class="icon-doc"></i> EDIT</span> - {{ t('notification_setting.event_pageEdit') }}
+                    <span class="label label-warning"><i class="icon-pencil"></i> EDIT</span> - {{ t('notification_setting.event_pageEdit') }}
                   </label>
                 </div>
-                <div class="checkbox checkbox-info">
-                  <input type="checkbox" id="trigger-event-pageDelete" name="notificationGlobal[triggerEvent:pageDelete]" value="pageDelete"
-                    {% if setting && (setting.triggerEvents.indexOf('pageDelete') != -1) %}checked{% endif %} />
-                  <label for="trigger-event-pageDelete">
-                    <span class="label label-info"><i class="icon-doc"></i> DELETE</span> - {{ t('notification_setting.event_pageDelete') }}
-                  </label>
-                </div>
-                <div class="checkbox checkbox-info">
+                <div class="checkbox checkbox-inverse">
                   <input type="checkbox" id="trigger-event-pageMove" name="notificationGlobal[triggerEvent:pageMove]" value="pageMove"
                     {% if setting && (setting.triggerEvents.indexOf('pageMove') != -1) %}checked{% endif %} />
                   <label for="trigger-event-pageMove">
-                    <span class="label label-info"><i class="icon-doc"></i> MOVE</span> - {{ t('notification_setting.event_pageMove') }}
+                    <span class="label label-warning"><i class="icon-action-redo"></i> MOVE</span> - {{ t('notification_setting.event_pageMove') }}
+                  </label>
+                </div>
+                <div class="checkbox checkbox-inverse">
+                  <input type="checkbox" id="trigger-event-pageDelete" name="notificationGlobal[triggerEvent:pageDelete]" value="pageDelete"
+                    {% if setting && (setting.triggerEvents.indexOf('pageDelete') != -1) %}checked{% endif %} />
+                  <label for="trigger-event-pageDelete">
+                    <span class="label label-danger"><i class="icon-fire"></i> DELETE</span> - {{ t('notification_setting.event_pageDelete') }}
                   </label>
                 </div>
-                <div class="checkbox checkbox-info">
+                <div class="checkbox checkbox-inverse">
                     <input type="checkbox" id="trigger-event-pageLike" name="notificationGlobal[triggerEvent:pageLike]" value="pageLike"
                       {% if setting && (setting.triggerEvents.indexOf('pageLike') != -1) %}checked{% endif %} />
                     <label for="trigger-event-pageLike">
-                      <span class="label label-info"><i class="icon-doc"></i> LIKE</span> - {{ t('notification_setting.event_pageLike') }}
+                      <span class="label label-info"><i class="icon-like"></i> LIKE</span> - {{ t('notification_setting.event_pageLike') }}
                     </label>
                   </div>
-                <div class="checkbox checkbox-info">
+                <div class="checkbox checkbox-inverse">
                   <input type="checkbox" id="trigger-event-comment" name="notificationGlobal[triggerEvent:comment]" value="comment"
                     {% if setting && (setting.triggerEvents.indexOf('comment') != -1) %}checked{% endif %} />
                   <label for="trigger-event-comment">
-                    <span class="label label-info"><i class="icon-fw icon-bubbles"></i> POST</span> - {{ t('notification_setting.event_comment') }}
+                    <span class="label label-default"><i class="icon-fw icon-bubble"></i> POST</span> - {{ t('notification_setting.event_comment') }}
                   </label>
                 </div>
               </div>

+ 6 - 6
lib/views/admin/global-notification.html

@@ -4,12 +4,12 @@
 <h2>{{ t('notification_setting.notification_list') }}</h2>
 
 {% set tags = {
-  pageCreate: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Create"><i class="icon-doc"></i> CREATE</span>',
-  pageEdit: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Edit"><i class="icon-doc"></i> EDIT</span>',
-  pageDelete: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Delte"><i class="icon-doc"></i> DELETE</span>',
-  pageMove: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Move"><i class="icon-doc"></i> MOVE</span>',
-  pageLike: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Like"><i class="icon-doc"></i> LIKE</span>',
-  comment: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="New Comment"><i class="icon-fw icon-bubbles"></i> POST</span>'
+  pageCreate: '<span class="label label-success" data-toggle="tooltip" data-placement="top" title="Page Create"><i class="icon-doc"></i> CREATE</span>',
+  pageEdit: '<span class="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Edit"><i class="icon-pencil"></i> EDIT</span>',
+  pageMove: '<span class="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Move"><i class="icon-action-redo"></i> MOVE</span>',
+  pageDelete: '<span class="label label-danger" data-toggle="tooltip" data-placement="top" title="Page Delte"><i class="icon-fire"></i> DELETE</span>',
+  pageLike: '<span class="label label-info" data-toggle="tooltip" data-placement="top" title="Page Like"><i class="icon-like"></i> LIKE</span>',
+  comment: '<span class="label label-default" data-toggle="tooltip" data-placement="top" title="New Comment"><i class="icon-fw icon-bubble"></i> POST</span>'
 } %}
 
 <table class="table table-bordered">

+ 1 - 0
lib/views/widget/page_tabs.html

@@ -58,6 +58,7 @@
         {% endif %}
       </ul>
     </li>
+    {% endif %}
   {% endif %}
 
   <li class="nav-main-right-tab pull-right">

+ 1 - 1
package.json

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

+ 25 - 0
resource/js/components/HeaderSearchBox/SearchForm.js

@@ -45,6 +45,30 @@ export default class SearchForm extends React.Component {
     }
   }
 
+  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);
   }
@@ -68,6 +92,7 @@ export default class SearchForm extends React.Component {
               onSubmit={this.onSubmit}
               emptyLabel={emptyLabel}
               placeholder="Search ..."
+              promptText={this.getHelpElement()}
             />
             <InputGroup.Button>
               <Button type="submit" bsStyle="link">

+ 209 - 12
resource/js/components/PageEditor/CodeMirrorEditor.js

@@ -1,6 +1,8 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
+import Modal from 'react-bootstrap/es/Modal';
+
 import AbstractEditor from './AbstractEditor';
 
 import urljoin from 'url-join';
@@ -19,6 +21,7 @@ window.CodeMirror = require('codemirror');
 
 
 import { UnControlled as ReactCodeMirror } from 'react-codemirror2';
+require('codemirror/addon/display/placeholder');
 require('codemirror/addon/edit/matchbrackets');
 require('codemirror/addon/edit/matchtags');
 require('codemirror/addon/edit/closetag');
@@ -58,6 +61,9 @@ export default class CodeMirrorEditor extends AbstractEditor {
       isGfmMode: this.props.isGfmMode,
       isEnabledEmojiAutoComplete: false,
       isLoadingKeymap: false,
+      isSimpleCheatsheetShown: this.props.isGfmMode && this.props.value.length === 0,
+      isCheatsheetModalButtonShown: this.props.isGfmMode && this.props.value.length > 0,
+      isCheatsheetModalShown: false,
       additionalClassSet: new Set(),
     };
 
@@ -77,8 +83,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
     this.scrollCursorIntoViewHandler = this.scrollCursorIntoViewHandler.bind(this);
     this.pasteHandler = this.pasteHandler.bind(this);
     this.cursorHandler = this.cursorHandler.bind(this);
+    this.changeHandler = this.changeHandler.bind(this);
+
+    this.updateCheatsheetStates = this.updateCheatsheetStates.bind(this);
 
     this.renderLoadingKeymapOverlay = this.renderLoadingKeymapOverlay.bind(this);
+    this.renderCheatsheetModalButton = this.renderCheatsheetModalButton.bind(this);
   }
 
   init() {
@@ -152,13 +162,13 @@ export default class CodeMirrorEditor extends AbstractEditor {
    */
   setGfmMode(bool) {
     // update state
-    const additionalClassSet = this.state.additionalClassSet;
     this.setState({
       isGfmMode: bool,
       isEnabledEmojiAutoComplete: bool,
-      additionalClassSet,
     });
 
+    this.updateCheatsheetStates(bool, null);
+
     // update CodeMirror option
     const mode = bool ? 'gfm' : undefined;
     this.getCodeMirror().setOption('mode', mode);
@@ -412,6 +422,19 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }
   }
 
+  changeHandler(editor, data, value) {
+    if (this.props.onChange != null) {
+      this.props.onChange(value);
+    }
+
+    this.updateCheatsheetStates(null, value);
+
+    // Emoji AutoComplete
+    if (this.state.isEnabledEmojiAutoComplete) {
+      this.emojiAutoCompleteHelper.showHint(editor);
+    }
+  }
+
   /**
    * CodeMirror paste event handler
    * see: https://codemirror.net/doc/manual.html#events
@@ -431,7 +454,26 @@ export default class CodeMirrorEditor extends AbstractEditor {
     }
   }
 
+  /**
+   * update states which related to cheatsheet
+   * @param {boolean} isGfmMode (use state.isGfmMode if null is set)
+   * @param {string} value (get value from codemirror if null is set)
+   */
+  updateCheatsheetStates(isGfmMode, value) {
+    if (isGfmMode == null) {
+      isGfmMode = this.state.isGfmMode;
+    }
+    if (value == null) {
+      value = this.getCodeMirror().getDoc().getValue();
+    }
+    // update isSimpleCheatsheetShown, isCheatsheetModalButtonShown
+    const isSimpleCheatsheetShown = isGfmMode && value.length === 0;
+    const isCheatsheetModalButtonShown = isGfmMode && value.length > 0;
+    this.setState({ isSimpleCheatsheetShown, isCheatsheetModalButtonShown });
+  }
+
   renderLoadingKeymapOverlay() {
+    // centering
     const style = {
       top: 0,
       right: 0,
@@ -448,6 +490,161 @@ export default class CodeMirrorEditor extends AbstractEditor {
       : '';
   }
 
+  renderSimpleCheatsheet() {
+    return (
+      <div className="panel panel-default gfm-cheatsheet mb-0">
+        <div className="panel-heading"><i className="icon-fw icon-question"/>Markdown Help</div>
+        <div className="panel-body small p-b-0">
+          <div className="row">
+            <div className="col-xs-6">
+              <p>
+                # 見出し1<br />
+                ## 見出し2
+              </p>
+              <p><i>*斜体*</i>&nbsp;&nbsp;<b>**強調**</b></p>
+              <p>
+                [リンク](http://..)<br />
+                [/ページ名/子ページ名]
+              </p>
+              <p>
+                ```javascript:index.js<br />
+                writeCode();<br />
+                ```
+              </p>
+            </div>
+            <div className="col-xs-6">
+              <p>
+                - リスト 1<br />
+                &nbsp;&nbsp;&nbsp;&nbsp;- リスト 1_1<br />
+                - リスト 2<br />
+                1. 番号付きリスト 1
+                1. 番号付きリスト 2
+              </p>
+              <hr />
+              <p>行末にスペース2つ[ ][ ]<br />で改行</p>
+            </div>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderCheatsheetModalBody() {
+    return (
+      <div className="row small">
+        <div className="col-sm-6">
+          <h4>Header</h4>
+          <ul className="hljs">
+            <li><code># </code>見出し1</li>
+            <li><code>## </code>見出し2</li>
+            <li><code>### </code>見出し3</li>
+          </ul>
+          <h4>Block</h4>
+          <p className="mb-1"><code>[空白行]</code>を挟むことで段落になります</p>
+          <ul className="hljs">
+            <li>text</li>
+            <li></li>
+            <li>text</li>
+          </ul>
+          <h4>Line breaks</h4>
+          <p className="mb-1">段落中、<code>[space][space]</code>(スペース2つ) で改行されます</p>
+          <ul className="hljs">
+            <li>text<code> </code><code> </code></li>
+            <li>text</li>
+          </ul>
+          <h4>Typography</h4>
+          <ul className="hljs">
+            <li><i>*イタリック*</i></li>
+            <li><b>**ボールド**</b></li>
+            <li><i><b>***イタリックボールド***</b></i></li>
+            <li>~~取り消し線~~ => <s>striked text</s></li>
+          </ul>
+          <h4>Link</h4>
+          <ul className="hljs">
+            <li>[Google](https://www.google.co.jp/)</li>
+            <li>[/Page1/ChildPage1]</li>
+          </ul>
+          <h4>コードハイライト</h4>
+          <ul className="hljs">
+            <li>```javascript:index.js</li>
+            <li>writeCode();</li>
+            <li>```</li>
+          </ul>
+        </div>
+        <div className="col-sm-6">
+          <h4>リスト</h4>
+          <ul className="hljs">
+            <li>- リスト 1</li>
+            <li>&nbsp;&nbsp;- リスト 1_1</li>
+            <li>- リスト 2</li>
+          </ul>
+          <ul className="hljs">
+            <li>1. 番号付きリスト 1</li>
+            <li>1. 番号付きリスト 2</li>
+          </ul>
+          <ul className="hljs">
+            <li>- [ ] タスク(チェックなし)</li>
+            <li>- [x] タスク(チェック付き)</li>
+          </ul>
+          <h4>引用</h4>
+          <ul className="hljs">
+            <li>> 複数行の引用文を</li>
+            <li>> 書くことができます</li>
+          </ul>
+          <ul className="hljs">
+            <li>>> 多重引用</li>
+            <li>>>> 多重引用</li>
+            <li>>>>> 多重引用</li>
+          </ul>
+          <h4>Table</h4>
+          <ul className="hljs text-center">
+            <li>|&nbsp;&nbsp;&nbsp;左寄せ&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;中央寄せ&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;右寄せ&nbsp;&nbsp;&nbsp;|</li>
+            <li>|:-----------|:----------:|-----------:|</li>
+            <li>|column 1&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;column 2&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;column 3|</li>
+            <li>|column 1&nbsp;&nbsp;&nbsp;&nbsp;|&nbsp;&nbsp;column 2&nbsp;&nbsp;|&nbsp;&nbsp;&nbsp;&nbsp;column 3|</li>
+          </ul>
+          <h4>Images</h4>
+          <p className="mb-1"><code> ![Alt文字列](URL)</code> で<span className="text-info">&lt;img&gt;</span>タグを挿入できます</p>
+          <ul className="hljs">
+            <li>![ex](https://example.com/images/a.png)</li>
+          </ul>
+
+          <hr />
+          <a href="/Sandbox" className="btn btn-info btn-block" target="_blank">
+            <i className="icon-share-alt"/> Sandbox を開く
+          </a>
+        </div>
+      </div>
+    );
+  }
+
+  renderCheatsheetModalButton() {
+    const showCheatsheetModal = () => {
+      this.setState({isCheatsheetModalShown: true});
+    };
+
+    const hideCheatsheetModal = () => {
+      this.setState({isCheatsheetModalShown: false});
+    };
+
+    return (
+      <React.Fragment>
+        <Modal className="modal-gfm-cheatsheet" show={this.state.isCheatsheetModalShown} onHide={() => { hideCheatsheetModal() }}>
+          <Modal.Header closeButton>
+            <Modal.Title><i className="icon-fw icon-question"/>Markdown Help</Modal.Title>
+          </Modal.Header>
+          <Modal.Body className="pt-1">
+            { this.renderCheatsheetModalBody() }
+          </Modal.Body>
+        </Modal>
+
+        <a className="gfm-cheatsheet-modal-link text-muted small" onClick={() => { showCheatsheetModal() }}>
+          <i className="icon-question" /> Markdown
+        </a>
+      </React.Fragment>
+    );
+  }
+
   render() {
     const mode = this.state.isGfmMode ? 'gfm' : undefined;
     const defaultEditorOptions = {
@@ -457,6 +654,8 @@ export default class CodeMirrorEditor extends AbstractEditor {
     const additionalClasses = Array.from(this.state.additionalClassSet).join(' ');
     const editorOptions = Object.assign(defaultEditorOptions, this.props.editorOptions || {});
 
+    const placeholder = this.state.isGfmMode ? 'Input with Markdown..' : 'Input with Plane Text..';
+
     return <React.Fragment>
       <ReactCodeMirror
         ref="cm"
@@ -478,6 +677,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
           lineWrapping: true,
           autoRefresh: {force: true},   // force option is enabled by autorefresh.ext.js -- Yuki Takei
           autoCloseTags: true,
+          placeholder: placeholder,
           matchBrackets: true,
           matchTags: {bothTags: true},
           // folding
@@ -506,16 +706,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             this.props.onScroll(data);
           }
         }}
-        onChange={(editor, data, value) => {
-          if (this.props.onChange != null) {
-            this.props.onChange(value);
-          }
-
-          // Emoji AutoComplete
-          if (this.state.isEnabledEmojiAutoComplete) {
-            this.emojiAutoCompleteHelper.showHint(editor);
-          }
-        }}
+        onChange={this.changeHandler}
         onDragEnter={(editor, event) => {
           if (this.props.onDragEnter != null) {
             this.props.onDragEnter(event);
@@ -524,6 +715,12 @@ export default class CodeMirrorEditor extends AbstractEditor {
       />
 
       { this.renderLoadingKeymapOverlay() }
+
+      <div className="overlay overlay-gfm-cheatsheet mt-1 p-3 pt-3">
+        { this.state.isSimpleCheatsheetShown && this.renderSimpleCheatsheet() }
+        { this.state.isCheatsheetModalButtonShown && this.renderCheatsheetModalButton() }
+      </div>
+
     </React.Fragment>;
   }
 

+ 2 - 2
resource/js/components/SearchPage/DeletePageListModal.js

@@ -43,8 +43,8 @@ export default class DeletePageListModal extends React.Component {
         <Modal.Footer>
           <div className="d-flex justify-content-between">
             <span className="text-danger">{this.props.errorMessage}</span>
-            <span className="d-flex">
-              <Checkbox onClick={this.props.toggleDeleteCompletely}>Delete completely</Checkbox>
+            <span className="d-flex align-items-center">
+              <Checkbox className="text-danger" onClick={this.props.toggleDeleteCompletely} inline={true}>Delete completely</Checkbox>
               <span className="m-l-10">
                 <Button onClick={this.props.confirmedToDelete}><i className="icon-trash"></i>Delete</Button>
               </span>

+ 35 - 23
resource/js/components/SearchPage/SearchResult.js

@@ -1,5 +1,6 @@
 import React from 'react';
 import PropTypes from 'prop-types';
+import * as toastr from 'toastr';
 
 import Page from '../PageList/Page';
 import SearchResultList from './SearchResultList';
@@ -106,32 +107,43 @@ export default class SearchResult extends React.Component {
    * @memberof SearchResult
    */
   deleteSelectedPages() {
-    let isDeleted = true;
     let deleteCompletely = this.state.isDeleteCompletely;
-    Array.from(this.state.selectedPages).map((page) => {
-      const pageId = page._id;
-      const revisionId = page.revision._id;
-      this.props.crowi.apiPost('/pages.remove',
-        {page_id: pageId, revision_id: revisionId, completely: deleteCompletely})
-      .then(res => {
-        if (res.ok) {
-          this.state.selectedPages.delete(page);
-        }
-        else {
-          isDeleted = false;
-        }
-      }).catch(err => {
-        /* eslint-disable no-console */
-        console.log(err.message);
-        /* eslint-enable */
-        isDeleted = false;
-        this.setState({errorMessageForDeleting: err.message});
+    Promise.all(Array.from(this.state.selectedPages).map((page) => {
+      return new Promise((resolve, reject) => {
+        const pageId = page._id;
+        const revisionId = page.revision._id;
+        this.props.crowi.apiPost('/pages.remove', {page_id: pageId, revision_id: revisionId, completely: deleteCompletely})
+          .then(res => {
+            if (res.ok) {
+              this.state.selectedPages.delete(page);
+              return resolve();
+            }
+            else {
+              return reject();
+            }
+          })
+          .catch(err => {
+            /* eslint-disable no-console */
+            console.log(err.message);
+            /* eslint-enable */
+            this.setState({errorMessageForDeleting: err.message});
+            return reject();
+          });
       });
-    });
-
-    if ( isDeleted ) {
+    }))
+    .then(() => {
       window.location.reload();
-    }
+    })
+    .catch(err => {
+      toastr.error(err, 'Error occured', {
+        closeButton: true,
+        progressBar: true,
+        newestOnTop: false,
+        showDuration: '100',
+        hideDuration: '100',
+        timeOut: '3000',
+      });
+    });
   }
 
   /**

+ 2 - 26
resource/js/components/SearchTypeahead.js

@@ -159,7 +159,6 @@ export default class SearchTypeahead extends React.Component {
     const defaultSelected = (this.props.keywordOnInit != '')
       ? [{path: this.props.keywordOnInit}]
       : [];
-    const help = this.getHelpElement();
 
     return (
       <div className="search-typeahead">
@@ -180,36 +179,12 @@ export default class SearchTypeahead extends React.Component {
           renderMenuItemChildren={this.renderMenuItemChildren}
           caseSensitive={false}
           defaultSelected={defaultSelected}
-          promptText={help}
+          promptText={this.props.promptText}
         />
         {restoreFormButton}
       </div>
     );
   }
-
-  getHelpElement() {
-    // TODO disabled temporary -- 2018.07.20 Yuki Takei
-    return <span>(TBD) Show Help</span>;
-    // return <table className="table table-borderd search-help">
-    //           <caption className="text-center">Search Help</caption>
-    //           <tr>
-    //             <td className="text-center">keyword</td>
-    //             <th>記事名 or カテゴリ or 本文にkeywordを含む</th>
-    //           </tr>
-    //           <tr>
-    //             <td className="text-center">title:keyword</td>
-    //             <th>記事名にkeywordを含む</th>
-    //           </tr>
-    //           <tr>
-    //             <td className="text-center">a b</td>
-    //             <th>文字列aとbを含む(スペース区切り)</th>
-    //           </tr>
-    //           <tr>
-    //             <td className="text-center">-keyword</td>
-    //             <th>文字列keywordを含まない</th>
-    //           </tr>
-    //         </table>;
-  }
 }
 
 /**
@@ -224,6 +199,7 @@ SearchTypeahead.propTypes = {
   emptyLabel:      PropTypes.string,
   placeholder:     PropTypes.string,
   keywordOnInit:   PropTypes.string,
+  promptText:      PropTypes.object,
 };
 
 /**

+ 7 - 0
resource/styles/scss/_comment_growi.scss

@@ -93,7 +93,14 @@
     display: block;
   }
 
+  // display cheatsheet for comment form only
   .comment-form {
+    .editor-cheatsheet {
+        display: none;
+    }
+
+
+
     position: relative;
     margin-top: 2em;
     // user icon

+ 48 - 6
resource/styles/scss/_editor-overlay.scss

@@ -1,9 +1,8 @@
 @mixin overlay-processing-style($additionalSelector, $contentFontSize: inherit, $contentPadding: inherit) {
   .overlay.#{$additionalSelector} {
-    background: rgba(255,255,255,0.5);
-
+    background: rgba(255, 255, 255, 0.5);
     .overlay-content {
-      background: rgba(200,200,200,0.5);
+      background: rgba(200, 200, 200, 0.5);
       color: #444;
       font-size: $contentFontSize;
       padding: $contentPadding;
@@ -18,20 +17,63 @@
     display: flex;
     justify-content: center;
     align-items: center;
-
     position: absolute;
-    z-index: 7;  // forward than .CodeMirror-vscrollbar
+    z-index: 7; // forward than .CodeMirror-vscrollbar
     top: 0;
     right: 0;
     bottom: 0;
     left: 0;
-
     .overlay-content {
       padding: 0.5em;
+      right: 0;
+      bottom: 0;
     }
   }
 
   // loading keymap
   @include overlay-processing-style(overlay-loading-keymap, 2.5em, 0.3em);
 
+  // cheat sheat
+  .overlay.overlay-gfm-cheatsheet {
+    justify-content: flex-end;
+    align-items: flex-end;
+
+    pointer-events: none;
+
+    .panel.gfm-cheatsheet {
+      opacity: 0.6;
+      box-shadow: unset;
+      .panel-body {
+        color: $text-muted;
+        font-family: monospace;
+        min-width: 30em;
+      }
+      ul > li {
+        list-style: none;
+      }
+    }
+
+    a.gfm-cheatsheet-modal-link {
+      pointer-events: all;
+      cursor: pointer;
+
+      opacity: 0.6;
+      color: $text-muted;
+
+      &:hover, &:focus {
+        opacity: 1;
+      }
+    }
+
+    // hide on smartphone
+    @media (max-width: $screen-xs) {
+      display: none;
+    }
+  }
+}
+
+.modal-gfm-cheatsheet .modal-body {
+  .hljs {
+    font-family: monospace;
+  }
 }

+ 5 - 3
resource/styles/scss/_on-edit.scss

@@ -147,7 +147,7 @@ body.on-edit {
     #page-editor {
       // right(preview)
       &,
-      .row,
+      &>.row,
       .page-editor-preview-container,
       .page-editor-preview-body {
         min-height: calc(100vh - #{$header-plus-footer});   // for IE11
@@ -162,6 +162,10 @@ body.on-edit {
         .textarea-editor {
           height: calc(100vh - #{$editor-margin});
         }
+
+        @media (min-width: $screen-md) {
+          padding-right: 0;
+        }
       }
     }
 
@@ -170,7 +174,6 @@ body.on-edit {
     *****************/
     .page-editor-editor-container {
       border-right: 1px solid transparent;
-      padding-right: 0;
       // override CodeMirror styles
       .CodeMirror {
         .cm-matchhighlight {
@@ -237,7 +240,6 @@ body.on-edit {
         min-width: 150px;
       }
     }
-
   } // .builtin-editor .tab-pane#edit
 
 

+ 6 - 7
resource/styles/scss/_search.scss

@@ -47,13 +47,6 @@
   }
 }
 
-// search help
-.search-help {
-  .search-help, td, th {
-    border: solid 1px gray;
-  }
-}
-
 // top and sidebar input styles
 .search-top, .search-sidebar {
   .search-clear {
@@ -99,6 +92,12 @@
       width: 300px;
     }
   }
+
+  table.search-help {
+    th, td {
+      border: none;
+    }
+  }
 }
 .search-sidebar {
   .search-form, .form-group, .rbt-input.form-control, .input-group {

+ 5 - 5
resource/styles/scss/theme/_override-agileadmin.scss

@@ -76,33 +76,33 @@ a {
  * Alert
  */
 .alert {
-  a {
+  a:not(.btn) {
     color: white;
   }
 
   &.alert-info {
-    a {
+    a:not(.btn) {
       &:hover, &:focus {
         color: lighten($info, 40%);
       }
     }
   }
   &.alert-success {
-    a {
+    a:not(.btn) {
       &:hover, &:focus {
         color: lighten($success, 40%);
       }
     }
   }
   &.alert-warning {
-    a {
+    a:not(.btn) {
       &:hover, &:focus {
         color: lighten($warning, 30%);
       }
     }
   }
   &.alert-danger {
-    a {
+    a:not(.btn) {
       &:hover, &:focus {
         color: lighten($danger, 30%);
       }