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

Merge branch 'support/apply-bootstrap4' into bst4-admin-customize

sooouh пре 6 година
родитељ
комит
58f1f21dad
33 измењених фајлова са 291 додато и 396 уклоњено
  1. 9 2
      CHANGES.md
  2. 1 1
      package.json
  3. 2 0
      src/client/js/admin.jsx
  4. 59 0
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  5. 9 13
      src/client/js/components/Admin/Common/AdminUpdateButtonRow.jsx
  6. 16 12
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  7. 6 6
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  8. 5 4
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  9. 1 2
      src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx
  10. 1 1
      src/client/js/components/Page/RevisionBody.jsx
  11. 49 41
      src/client/js/components/Page/RevisionRenderer.jsx
  12. 5 44
      src/client/js/components/PageEditor.jsx
  13. 75 2
      src/client/js/components/PageEditor/Preview.jsx
  14. 2 3
      src/client/styles/agile-admin/inverse/colors/spring.scss
  15. 1 6
      src/client/styles/scss/_variables.scss
  16. 3 22
      src/server/views/admin/app.html
  17. 3 26
      src/server/views/admin/customize.html
  18. 3 12
      src/server/views/admin/export.html
  19. 3 30
      src/server/views/admin/external-accounts.html
  20. 5 23
      src/server/views/admin/global-notification-detail.html
  21. 3 32
      src/server/views/admin/importer.html
  22. 3 18
      src/server/views/admin/index.html
  23. 3 7
      src/server/views/admin/markdown.html
  24. 3 7
      src/server/views/admin/notification.html
  25. 2 8
      src/server/views/admin/search.html
  26. 2 5
      src/server/views/admin/security.html
  27. 7 11
      src/server/views/admin/user-group-detail.html
  28. 3 7
      src/server/views/admin/user-groups.html
  29. 2 18
      src/server/views/admin/users.html
  30. 0 16
      src/server/views/admin/widget/menu.html
  31. 0 16
      src/server/views/admin/widget/theme-colorbox.html
  32. 4 0
      src/server/views/layout/admin.html
  33. 1 1
      src/server/views/widget/passport/ldap-association-tester.html

+ 9 - 2
CHANGES.md

@@ -1,8 +1,15 @@
 # CHANGES
 
-## 3.6.7-RC
+## v3.6.8-RC
 
-* Imprv: Show error toastr when saving page is failed because of empty document
+* Improvement: Optimize markdown rendering
+
+## v3.6.7
+
+* Feature: Anchor link for comments
+* Improvement: Show error toastr when saving page is failed because of empty document
+* Fix: Admin Customise couldn't restore stored config value
+    * Introduced by 3.6.2
 * Fix: Admin Customise missed preview functions
     * Introduced by 3.6.2
 * Fix: AWS doesn't work

+ 1 - 1
package.json

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

+ 2 - 0
src/client/js/admin.jsx

@@ -18,6 +18,7 @@ import Customize from './components/Admin/Customize/Customize';
 import ImportDataPage from './components/Admin/ImportDataPage';
 import ExportArchiveDataPage from './components/Admin/ExportArchiveDataPage';
 import FullTextSearchManagement from './components/Admin/FullTextSearchManagement';
+import AdminNavigation from './components/Admin/Common/AdminNavigation';
 
 import AdminHomeContainer from './services/AdminHomeContainer';
 import AdminCustomizeContainer from './services/AdminCustomizeContainer';
@@ -79,6 +80,7 @@ Object.assign(componentMappings, {
   'admin-user-group-detail': <UserGroupDetailPage />,
   'admin-full-text-search-management': <FullTextSearchManagement />,
   'admin-user-group-page': <UserGroupPage />,
+  'admin-navigation': <AdminNavigation />,
 });
 
 

+ 59 - 0
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -0,0 +1,59 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import urljoin from 'url-join';
+
+const AdminNavigation = (props) => {
+  const { t } = props;
+  const pathname = window.location.pathname;
+
+  const isActiveMenu = (path) => {
+    return (pathname.startsWith(urljoin('/admin', path)));
+  };
+
+  return (
+    <ul className="nav nav-pills nav-stacked">
+      <li className={`${pathname === '/admin' && 'active'}`}>
+        <a href="/admin"><i className="icon-fw icon-home"></i> { t('Management Wiki Home') }</a>
+      </li>
+      <li className={`${isActiveMenu('/app') && 'active'}`}>
+        <a href="/admin/app"><i className="icon-fw icon-settings"></i> { t('App Settings') }</a>
+      </li>
+      <li className={`${isActiveMenu('/security') && 'active'}`}>
+        <a href="/admin/security"><i className="icon-fw icon-shield"></i> { t('security_settings') }</a>
+      </li>
+      <li className={`${isActiveMenu('/markdown') && 'active'}`}>
+        <a href="/admin/markdown"><i className="icon-fw icon-note"></i> { t('Markdown Settings') }</a>
+      </li>
+      <li className={`${isActiveMenu('/customize') && 'active'}`}>
+        <a href="/admin/customize"><i className="icon-fw icon-wrench"></i> { t('Customize') }</a>
+      </li>
+      <li className={`${isActiveMenu('/importer') && 'active'}`}>
+        <a href="/admin/importer"><i className="icon-fw icon-cloud-upload"></i> { t('Import Data') }</a>
+      </li>
+      <li className={`${isActiveMenu('/export') && 'active'}`}>
+        <a href="/admin/export"><i className="icon-fw icon-cloud-download"></i> { t('Export Archive Data') }</a>
+      </li>
+      <li className={`${(isActiveMenu('/notification') || isActiveMenu('/global-notification')) && 'active'}`}>
+        <a href="/admin/notification"><i className="icon-fw icon-bell"></i> { t('Notification Settings') }</a>
+      </li>
+      <li className={`${(isActiveMenu('/users')) && 'active'}`}>
+        <a href="/admin/users"><i className="icon-fw icon-user"></i> { t('User_Management') }</a>
+      </li>
+      <li className={`${isActiveMenu('/user-group') && 'active'}`}>
+        <a href="/admin/user-groups"><i className="icon-fw icon-people"></i> { t('UserGroup Management') }</a>
+      </li>
+      <li className={`${isActiveMenu('/search') && 'active'}`}>
+        <a href="/admin/search"><i className="icon-fw icon-magnifier"></i> { t('Full Text Search Management') }</a>
+      </li>
+    </ul>
+  );
+};
+
+
+AdminNavigation.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+};
+
+export default withTranslation()(AdminNavigation);

+ 9 - 13
src/client/js/components/Admin/Common/AdminUpdateButtonRow.jsx

@@ -2,21 +2,17 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-class AdminUpdateButtonRow extends React.PureComponent {
+const AdminUpdateButtonRow = (props) => {
+  const { t } = props;
 
-  render() {
-    const { t } = this.props;
-
-    return (
-      <div className="row my-3">
-        <div className="col-xs-offset-4 col-xs-5">
-          <button type="button" className="btn btn-primary" onClick={this.props.onClick} disabled={this.props.disabled}>{ t('Update') }</button>
-        </div>
+  return (
+    <div className="row my-3">
+      <div className="offset-4 col-5">
+        <button type="button" className="btn btn-primary" onClick={props.onClick} disabled={props.disabled}>{ t('Update') }</button>
       </div>
-    );
-  }
-
-}
+    </div>
+  );
+};
 
 AdminUpdateButtonRow.propTypes = {
   t: PropTypes.func.isRequired, // i18next

+ 16 - 12
src/client/js/components/Admin/Notification/GlobalNotificationList.jsx

@@ -76,44 +76,48 @@ class GlobalNotificationList extends React.Component {
           return (
             <tr key={notification._id}>
               <td className="align-middle td-abs-center">
-                <input
-                  id="isNotificationEnabled"
-                  type="checkbox"
-                  defaultChecked={notification.isEnabled}
-                  onClick={e => this.toggleIsEnabled(notification)}
-                />
+                <div className="custom-control custom-switch checkbox-success">
+                  <input
+                    type="checkbox"
+                    className="custom-control-input"
+                    id={notification._id}
+                    defaultChecked={notification.isEnabled}
+                    onClick={() => this.toggleIsEnabled(notification)}
+                  />
+                  <label className="custom-control-label" htmlFor={notification._id} />
+                </div>
               </td>
               <td>
                 {notification.triggerPath}
               </td>
               <td>
                 {notification.triggerEvents.includes('pageCreate') && (
-                  <span className="label label-success" data-toggle="tooltip" data-placement="top" title="Page Create">
+                  <span className="badge badge-pill badge-success" data-toggle="tooltip" data-placement="top" title="Page Create">
                     <i className="icon-doc"></i> CREATE
                   </span>
                 )}
                 {notification.triggerEvents.includes('pageEdit') && (
-                  <span className="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
+                  <span className="badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Edit">
                     <i className="icon-pencil"></i> EDIT
                   </span>
                 )}
                 {notification.triggerEvents.includes('pageMove') && (
-                  <span className="label label-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
+                  <span className="badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
                     <i className="icon-action-redo"></i> MOVE
                   </span>
                 )}
                 {notification.triggerEvents.includes('pageDelete') && (
-                  <span className="label label-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
+                  <span className="badge badge-pill badge-danger" data-toggle="tooltip" data-placement="top" title="Page Delte">
                     <i className="icon-fire"></i> DELETE
                   </span>
                 )}
                 {notification.triggerEvents.includes('pageLike') && (
-                  <span className="label label-info" data-toggle="tooltip" data-placement="top" title="Page Like">
+                  <span className="badge badge-pill badge-info" data-toggle="tooltip" data-placement="top" title="Page Like">
                     <i className="icon-like"></i> LIKE
                   </span>
                 )}
                 {notification.triggerEvents.includes('comment') && (
-                  <span className="label label-default" data-toggle="tooltip" data-placement="top" title="New Comment">
+                  <span className="badge badge-pill badge-light" data-toggle="tooltip" data-placement="top" title="New Comment">
                     <i className="icon-fw icon-bubble"></i> POST
                   </span>
                 )}

+ 6 - 6
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -199,7 +199,7 @@ class ManageGlobalNotification extends React.Component {
                 checked={this.state.triggerEvents.has('pageCreate')}
                 onChange={() => this.onChangeTriggerEvents('pageCreate')}
               >
-                <span className="label label-success">
+                <span className="badge badge-pill badge-success">
                   <i className="icon-doc"></i> CREATE
                 </span>
               </TriggerEventCheckBox>
@@ -208,7 +208,7 @@ class ManageGlobalNotification extends React.Component {
                 checked={this.state.triggerEvents.has('pageEdit')}
                 onChange={() => this.onChangeTriggerEvents('pageEdit')}
               >
-                <span className="label label-warning">
+                <span className="badge badge-pill badge-warning">
                   <i className="icon-pencil"></i>EDIT
                 </span>
               </TriggerEventCheckBox>
@@ -217,7 +217,7 @@ class ManageGlobalNotification extends React.Component {
                 checked={this.state.triggerEvents.has('pageMove')}
                 onChange={() => this.onChangeTriggerEvents('pageMove')}
               >
-                <span className="label label-warning">
+                <span className="badge badge-pill badge-warning">
                   <i className="icon-action-redo"></i>MOVE
                 </span>
               </TriggerEventCheckBox>
@@ -226,7 +226,7 @@ class ManageGlobalNotification extends React.Component {
                 checked={this.state.triggerEvents.has('pageDelete')}
                 onChange={() => this.onChangeTriggerEvents('pageDelete')}
               >
-                <span className="label label-danger">
+                <span className="badge badge-pill badge-danger">
                   <i className="icon-fire"></i>DELETE
                 </span>
               </TriggerEventCheckBox>
@@ -235,7 +235,7 @@ class ManageGlobalNotification extends React.Component {
                 checked={this.state.triggerEvents.has('pageLike')}
                 onChange={() => this.onChangeTriggerEvents('pageLike')}
               >
-                <span className="label label-info">
+                <span className="badge badge-pill badge-info">
                   <i className="icon-like"></i>LIKE
                 </span>
               </TriggerEventCheckBox>
@@ -244,7 +244,7 @@ class ManageGlobalNotification extends React.Component {
                 checked={this.state.triggerEvents.has('comment')}
                 onChange={() => this.onChangeTriggerEvents('comment')}
               >
-                <span className="label label-default">
+                <span className="badge badge-pill badge-light">
                   <i className="icon-bubble"></i>POST
                 </span>
               </TriggerEventCheckBox>

+ 5 - 4
src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx

@@ -86,14 +86,15 @@ class SlackAppConfiguration extends React.Component {
 
             <div className="row mb-5">
               <div className="col-xs-offset-3 col-xs-6 text-left">
-                <div className="checkbox checkbox-success">
+                <div className="custom-control custom-switch checkbox-success">
                   <input
-                    id="cbPrioritizeIWH"
                     type="checkbox"
+                    className="custom-control-input"
+                    id="cbPrioritizeIWH"
                     checked={adminNotificationContainer.state.isIncomingWebhookPrioritized}
                     onChange={() => { adminNotificationContainer.switchIsIncomingWebhookPrioritized() }}
                   />
-                  <label htmlFor="cbPrioritizeIWH">
+                  <label className="custom-control-label" htmlFor="cbPrioritizeIWH">
                     {t('notification_setting.prioritize_webhook')}
                   </label>
                 </div>
@@ -120,7 +121,7 @@ class SlackAppConfiguration extends React.Component {
                   onClick={() => adminNotificationContainer.switchSlackOption('Incoming Webhooks')}
                 >
                   {t('notification_setting.use_instead')}
-                </a>{' '}
+                </a>
               </div>
 
               <div className="row mb-5">

+ 1 - 2
src/client/js/components/Admin/Notification/TriggerEventCheckBox.jsx

@@ -6,11 +6,10 @@ const TriggerEventCheckBox = (props) => {
   const { t } = props;
 
   return (
-    <div className="checkbox checkbox-inverse">
+    <div className="checkbox">
       <input
         type="checkbox"
         id={`trigger-event-${props.event}`}
-        value={props.event}
         checked={props.checked}
         onChange={props.onChange}
       />

+ 1 - 1
src/client/js/components/Page/RevisionBody.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import { debounce } from 'throttle-debounce';
 
-export default class RevisionBody extends React.Component {
+export default class RevisionBody extends React.PureComponent {
 
   constructor(props) {
     super(props);

+ 49 - 41
src/client/js/components/Page/RevisionRenderer.jsx

@@ -8,7 +8,7 @@ import GrowiRenderer from '../../util/GrowiRenderer';
 
 import RevisionBody from './RevisionBody';
 
-class RevisionRenderer extends React.Component {
+class RevisionRenderer extends React.PureComponent {
 
   constructor(props) {
     super(props);
@@ -21,12 +21,32 @@ class RevisionRenderer extends React.Component {
     this.getHighlightedBody = this.getHighlightedBody.bind(this);
   }
 
-  componentWillMount() {
-    this.renderHtml(this.props.markdown, this.props.highlightKeywords);
+  initCurrentRenderingContext() {
+    this.currentRenderingContext = {
+      markdown: this.props.markdown,
+      currentPagePath: this.props.pageContainer.state.path,
+    };
+  }
+
+  componentDidMount() {
+    this.initCurrentRenderingContext();
+    this.renderHtml();
   }
 
-  componentWillReceiveProps(nextProps) {
-    this.renderHtml(nextProps.markdown, this.props.highlightKeywords);
+  componentDidUpdate(prevProps) {
+    const { markdown: prevMarkdown, highlightKeywords: prevHighlightKeywords } = prevProps;
+    const { markdown, highlightKeywords } = this.props;
+
+    // render only when props.markdown is updated
+    if (markdown !== prevMarkdown || highlightKeywords !== prevHighlightKeywords) {
+      this.initCurrentRenderingContext();
+      this.renderHtml();
+      return;
+    }
+
+    const { interceptorManager } = this.props.appContainer;
+
+    interceptorManager.process('postRenderHtml', this.currentRenderingContext);
   }
 
   /**
@@ -51,42 +71,30 @@ class RevisionRenderer extends React.Component {
     return returnBody;
   }
 
-  renderHtml(markdown) {
-    const { pageContainer } = this.props;
-
-    const context = {
-      markdown,
-      currentPagePath: pageContainer.state.path,
-    };
-
-    const growiRenderer = this.props.growiRenderer;
-    const interceptorManager = this.props.appContainer.interceptorManager;
-    interceptorManager.process('preRender', context)
-      .then(() => { return interceptorManager.process('prePreProcess', context) })
-      .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown);
-      })
-      .then(() => { return interceptorManager.process('postPreProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.process(context.markdown);
-      })
-      .then(() => { return interceptorManager.process('prePostProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
-
-        // highlight
-        if (this.props.highlightKeywords != null) {
-          context.parsedHTML = this.getHighlightedBody(context.parsedHTML, this.props.highlightKeywords);
-        }
-      })
-      .then(() => { return interceptorManager.process('postPostProcess', context) })
-      .then(() => { return interceptorManager.process('preRenderHtml', context) })
-      .then(() => {
-        this.setState({ html: context.parsedHTML });
-      })
-      // process interceptors for post rendering
-      .then(() => { return interceptorManager.process('postRenderHtml', context) });
-
+  async renderHtml() {
+    const {
+      appContainer, growiRenderer,
+      highlightKeywords,
+    } = this.props;
+
+    const { interceptorManager } = appContainer;
+    const context = this.currentRenderingContext;
+
+    await interceptorManager.process('preRender', context);
+    await interceptorManager.process('prePreProcess', context);
+    context.markdown = growiRenderer.preProcess(context.markdown);
+    await interceptorManager.process('postPreProcess', context);
+    context.parsedHTML = growiRenderer.process(context.markdown);
+    await interceptorManager.process('prePostProcess', context);
+    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+
+    if (this.props.highlightKeywords != null) {
+      context.parsedHTML = this.getHighlightedBody(context.parsedHTML, highlightKeywords);
+    }
+    await interceptorManager.process('postPostProcess', context);
+    await interceptorManager.process('preRenderHtml', context);
+
+    this.setState({ html: context.parsedHTML });
   }
 
   render() {

+ 5 - 44
src/client/js/components/PageEditor.jsx

@@ -44,9 +44,6 @@ class PageEditor extends React.Component {
     this.saveDraft = this.saveDraft.bind(this);
     this.clearDraft = this.clearDraft.bind(this);
 
-    // get renderer
-    this.growiRenderer = this.props.appContainer.getRenderer('editor');
-
     // for scrolling
     this.lastScrolledDateWithCursor = null;
     this.isOriginOfScrollSyncEditor = false;
@@ -56,15 +53,14 @@ class PageEditor extends React.Component {
     this.scrollPreviewByEditorLineWithThrottle = throttle(20, this.scrollPreviewByEditorLine);
     this.scrollPreviewByCursorMovingWithThrottle = throttle(20, this.scrollPreviewByCursorMoving);
     this.scrollEditorByPreviewScrollWithThrottle = throttle(20, this.scrollEditorByPreviewScroll);
-    this.renderPreviewWithDebounce = debounce(50, throttle(100, this.renderPreview));
+    this.setMarkdownStateWithDebounce = debounce(50, throttle(100, (value) => {
+      this.setState({ markdown: value });
+    }));
     this.saveDraftWithDebounce = debounce(800, this.saveDraft);
   }
 
   componentWillMount() {
     this.props.appContainer.registerComponentInstance('PageEditor', this);
-
-    // initial rendering
-    this.renderPreview(this.state.markdown);
   }
 
   getMarkdown() {
@@ -93,7 +89,7 @@ class PageEditor extends React.Component {
    * @param {string} value
    */
   onMarkdownChanged(value) {
-    this.renderPreviewWithDebounce(value);
+    this.setMarkdownStateWithDebounce(value);
     this.saveDraftWithDebounce();
   }
 
@@ -285,41 +281,6 @@ class PageEditor extends React.Component {
     this.props.editorContainer.clearDraft(this.props.pageContainer.state.path);
   }
 
-  renderPreview(value) {
-    this.setState({ markdown: value });
-
-    // render html
-    const context = {
-      markdown: this.state.markdown,
-      currentPagePath: decodeURIComponent(window.location.pathname),
-    };
-
-    const growiRenderer = this.growiRenderer;
-    const interceptorManager = this.props.appContainer.interceptorManager;
-    interceptorManager.process('preRenderPreview', context)
-      .then(() => { return interceptorManager.process('prePreProcess', context) })
-      .then(() => {
-        context.markdown = growiRenderer.preProcess(context.markdown);
-      })
-      .then(() => { return interceptorManager.process('postPreProcess', context) })
-      .then(() => {
-        const parsedHTML = growiRenderer.process(context.markdown);
-        context.parsedHTML = parsedHTML;
-      })
-      .then(() => { return interceptorManager.process('prePostProcess', context) })
-      .then(() => {
-        context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
-      })
-      .then(() => { return interceptorManager.process('postPostProcess', context) })
-      .then(() => { return interceptorManager.process('preRenderPreviewHtml', context) })
-      .then(() => {
-        this.setState({ html: context.parsedHTML });
-      })
-      // process interceptors for post rendering
-      .then(() => { return interceptorManager.process('postRenderPreviewHtml', context) });
-
-  }
-
   render() {
     const config = this.props.appContainer.getConfig();
     const noCdn = envUtils.toBoolean(config.env.NO_CDN);
@@ -345,7 +306,7 @@ class PageEditor extends React.Component {
         </div>
         <div className="col-md-6 hidden-sm hidden-xs page-editor-preview-container">
           <Preview
-            html={this.state.html}
+            markdown={this.state.markdown}
             // eslint-disable-next-line no-return-assign
             inputRef={(el) => { return this.previewElement = el }}
             isMathJaxEnabled={this.state.isMathJaxEnabled}

+ 75 - 2
src/client/js/components/PageEditor/Preview.jsx

@@ -2,15 +2,76 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { Subscribe } from 'unstated';
+import { createSubscribedElement } from '../UnstatedUtils';
 
 import RevisionBody from '../Page/RevisionBody';
 
+import AppContainer from '../../services/AppContainer';
 import EditorContainer from '../../services/EditorContainer';
 
 /**
  * Wrapper component for Page/RevisionBody
  */
-export default class Preview extends React.PureComponent {
+class Preview extends React.PureComponent {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      html: '',
+    };
+
+    // get renderer
+    this.growiRenderer = props.appContainer.getRenderer('editor');
+  }
+
+  componentDidMount() {
+    this.initCurrentRenderingContext();
+    this.renderPreview();
+  }
+
+  componentDidUpdate(prevProps) {
+    const { markdown: prevMarkdown } = prevProps;
+    const { markdown } = this.props;
+
+    // render only when props.markdown is updated
+    if (markdown !== prevMarkdown) {
+      this.initCurrentRenderingContext();
+      this.renderPreview();
+      return;
+    }
+
+    const { interceptorManager } = this.props.appContainer;
+
+    interceptorManager.process('postRenderPreviewHtml', this.currentRenderingContext);
+  }
+
+  initCurrentRenderingContext() {
+    this.currentRenderingContext = {
+      markdown: this.props.markdown,
+      currentPagePath: decodeURIComponent(window.location.pathname),
+    };
+  }
+
+  async renderPreview() {
+    const { appContainer } = this.props;
+    const { growiRenderer } = this;
+
+    const { interceptorManager } = appContainer;
+    const context = this.currentRenderingContext;
+
+    await interceptorManager.process('preRenderPreview', context);
+    await interceptorManager.process('prePreProcess', context);
+    context.markdown = growiRenderer.preProcess(context.markdown);
+    await interceptorManager.process('postPreProcess', context);
+    context.parsedHTML = growiRenderer.process(context.markdown);
+    await interceptorManager.process('prePostProcess', context);
+    context.parsedHTML = growiRenderer.postProcess(context.parsedHTML);
+    await interceptorManager.process('postPostProcess', context);
+    await interceptorManager.process('preRenderPreviewHtml', context);
+
+    this.setState({ html: context.parsedHTML });
+  }
 
   render() {
     return (
@@ -31,6 +92,7 @@ export default class Preview extends React.PureComponent {
           >
             <RevisionBody
               {...this.props}
+              html={this.state.html}
               renderMathJaxInRealtime={editorContainer.state.previewOptions.renderMathJaxInRealtime}
             />
           </div>
@@ -41,10 +103,21 @@ export default class Preview extends React.PureComponent {
 
 }
 
+/**
+ * Wrapper component for using unstated
+ */
+const PreviewWrapper = (props) => {
+  return createSubscribedElement(Preview, props, [AppContainer]);
+};
+
 Preview.propTypes = {
-  html: PropTypes.string,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  markdown: PropTypes.string,
   inputRef: PropTypes.func.isRequired, // for getting div element
   isMathJaxEnabled: PropTypes.bool,
   renderMathJaxOnInit: PropTypes.bool,
   onScroll: PropTypes.func,
 };
+
+export default PreviewWrapper;

+ 2 - 3
src/client/styles/agile-admin/inverse/colors/spring.scss

@@ -20,10 +20,9 @@ $background-color: rgba(171, 224, 174, 0.4);
 $third-main-color: antiquewhite;
 $textcolor: dimgray;
 $primary: $themecolor;
-
 $logo-mark-fill: lighten(desaturate($topbar, 10%), 15%);
-$wikilinktext: lighten($themecolor, 20%);
-$wikilinktext-hover: lighten($wikilinktext, 20%);
+$wikilinktext: $subthemecolor;
+$wikilinktext-hover: gba(171, 224, 174, 0.9);
 
 @import 'apply-colors';
 @import 'apply-colors-light';

+ 1 - 6
src/client/styles/scss/_variables.scss

@@ -2,12 +2,7 @@
 $growi-green: #74bc46;
 $growi-blue: #175fa5;
 
-$font-family-monospace-not-strictly: Monaco,
-  Menlo,
-  Consolas,
-  'Courier New',
-  MeiryoKe_Gothic,
-  monospace;
+$font-family-monospace-not-strictly: Monaco, Menlo, Consolas, 'Courier New', MeiryoKe_Gothic, monospace;
 
 //== Layout
 $grw-navbar-height: 50px;

+ 3 - 22
src/server/views/admin/app.html

@@ -14,28 +14,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
-  </div>
-  {% endif %}
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'app'} %}
-    </div>
-
-    <div class="col-md-9" id="admin-app"></div>
-  </div>
-
+<div class="content-main row">
+  {% parent %}
+  <div class="col-md-9" id="admin-app"></div>
 </div>
 {% endblock content_main %}
 

+ 3 - 26
src/server/views/admin/customize.html

@@ -21,32 +21,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main admin-customize">
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
-  </div>
-  {% endif %}
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div id="grw-hljs-container-for-demo">
-    {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
-  </div>
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'customize'} %}
-    </div>
-    <div class="col-md-9">
-      <div id="admin-customize"></div>
-    </div>
-  </div>
+<div class="content-main admin-customize row">
+  {% parent %}
+  <div class="col-md-9" id="admin-customize"></div>
 </div>
 {% endblock content_main %}
 

+ 3 - 12
src/server/views/admin/export.html

@@ -11,18 +11,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main admin-export">
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'export'} %}
-    </div>
-    <div
-      id="admin-export-page"
-      class="col-md-9"
-    >
-    </div>
-  </div>
+<div class="content-main admin-export row">
+  {% parent %}
+  <div id="admin-export-page" class="col-md-9"></div>
 </div>
 
 {% endblock content_main %}

+ 3 - 30
src/server/views/admin/external-accounts.html

@@ -11,36 +11,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-  {% set wmessage = req.flash('warningMessage') %}
-  {% if wmessage.length %}
-  <div class="alert alert-warning">
-    {{ wmessage }}
-  </div>
-  {% endif %}
-
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
-  </div>
-  {% endif %}
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'external-account'} %}
-    </div>
-
-    <div class="col-md-9" id="admin-external-account-setting">
-    </div>
-  </div>
+<div class="content-main row">
+  {% parent %}
+  <div class="col-md-9" id="admin-external-account-setting"></div>
 </div>
 {% endblock content_main %}
 

+ 5 - 23
src/server/views/admin/global-notification-detail.html

@@ -11,29 +11,11 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-  {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
-  </div>
-  {% endif %}
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'notification'} %}
-    </div>
-    <div class="col-md-9" id="admin-global-notification-setting"
-      data-global-notification="{{ globalNotification|json }}">
-    </div>
-  </div>
+<div class="content-main row">
+  {% parent %}
+  <div class="col-md-9" id="admin-global-notification-setting"
+      data-global-notification="{{ globalNotification|json }}"></div>
+</div>
 
   {% endblock content_main %}
 

+ 3 - 32
src/server/views/admin/importer.html

@@ -11,38 +11,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main admin-importer">
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'importer'} %}
-    </div>
-    <div class="col-lg-7 col-md-9">
-
-      <!-- Flash message for success -->
-      {% set smessage = req.flash('successMessage') %}
-      {% if smessage.length %}
-      <div class="alert alert-success">
-        {% for e in smessage %}
-        {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <!-- Flash message for error -->
-      {% set emessage = req.flash('errorMessage') %}
-      {% if emessage.length %}
-      <div class="alert alert-danger">
-        {% for e in emessage %}
-        {{ e }}<br>
-        {% endfor %}
-      </div>
-      {% endif %}
-
-      <div id="admin-importer"></div>
-
-    </div>
-  </div>
+<div class="content-main admin-importer row">
+  {% parent %}
+  <div class="col-md-9" id="admin-importer"></div>
 </div>
 
 {% endblock content_main %}

+ 3 - 18
src/server/views/admin/index.html

@@ -11,24 +11,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' %}
-    </div>
-    <div class="col-md-9">
-      <div id="admin-home"></div>
-    </div>
-  </div>
-
+<div class="content-main row">
+  {% parent %}
+  <div class="col-md-9" id="admin-home"></div>
 </div>
 {% endblock content_main %}
 

+ 3 - 7
src/server/views/admin/markdown.html

@@ -12,13 +12,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'markdown'} %}
-    </div>
-    <div class="col-md-9" id="admin-markdown-setting"></div>
-  </div>
+<div class="content-main row">
+  {% parent %}
+  <div class="col-md-9" id="admin-markdown-setting"></div>
 </div>
 
 {% endblock content_main %}

+ 3 - 7
src/server/views/admin/notification.html

@@ -11,13 +11,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main admin-notification">
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'notification'} %}
-    </div>
-    <div class="col-md-9" id="admin-notification-setting"></div>
-  </div>
+<div class="content-main admin-notification row">
+  {% parent %}
+  <div class="col-md-9" id="admin-notification-setting"></div>
 </div>
 {% endblock content_main %}
 

+ 2 - 8
src/server/views/admin/search.html

@@ -11,12 +11,8 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'search'} %}
-    </div>
+<div class="content-main row">
+  {% parent %}
     <div
       class="col-md-9"
       id ="admin-full-text-search-management"
@@ -25,8 +21,6 @@
       <!-- {% include '../widget/pager.html' with {path: "/admin/search", pager: pager} %} -->
       <!-- Reactify Paginator end -->
     </div>
-  </div>
-
 </div>
 
 

+ 2 - 5
src/server/views/admin/security.html

@@ -11,11 +11,8 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main admin-security">
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'security'} %}
-    </div>
+<div class="content-main admin-security row">
+  {% parent %}
     <div class="col-md-9">
 
       {% set smessage = req.flash('successMessage') %}

+ 7 - 11
src/server/views/admin/user-group-detail.html

@@ -11,17 +11,13 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'user-group'} %}
-    </div>
-    <div
-      id="admin-user-group-detail"
-      class="col-md-9"
-      data-user-group="{{ userGroup|json }}"
-    >
-    </div>
+<div class="content-main row">
+  {% parent %}
+  <div
+    id="admin-user-group-detail"
+    class="col-md-9"
+    data-user-group="{{ userGroup|json }}"
+  >
   </div>
 </div>
 {% endblock content_main %}

+ 3 - 7
src/server/views/admin/user-groups.html

@@ -11,13 +11,9 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-  <div class="row">
-    <div class="col-md-3">
-      {% include './widget/menu.html' with {current: 'user-group'} %}
-    </div>
-    <div id ="admin-user-group-page" class="col-md-9"></div>
-  </div>
+<div class="content-main row">
+  {% parent %}
+  <div id ="admin-user-group-page" class="col-md-9"></div>
 </div>
 {% endblock content_main %}
 

+ 2 - 18
src/server/views/admin/users.html

@@ -11,24 +11,8 @@
 {% endblock %}
 
 {% block content_main %}
-<div class="content-main">
-    {% set smessage = req.flash('successMessage') %}
-  {% if smessage.length %}
-  <div class="alert alert-success">
-    {{ smessage }}
-  </div>
-  {% endif %}
-
-  {% set emessage = req.flash('errorMessage') %}
-  {% if emessage.length %}
-  <div class="alert alert-danger">
-    {{ emessage }}
-  </div>
-  {% endif %}
-
-  <div class="col-md-3">
-    {% include './widget/menu.html' with {current: 'user'} %}
-  </div>
+<div class="content-main row">
+  {% parent %}
   <div
     class="col-md-9"
     id ="admin-user-page"

+ 0 - 16
src/server/views/admin/widget/menu.html

@@ -1,16 +0,0 @@
-{% if not current %}
-  {% set current = 'index' %}
-{% endif  %}
-<ul class="nav nav-pills nav-stacked">
-  <li class="{% if current == 'index'%}active{% endif %}"><a href="/admin"><i class="icon-fw icon-home"></i> {{ t('Management Wiki Home') }}</a></li>
-  <li class="{% if current == 'app'%}active{% endif %}"><a href="/admin/app"><i class="icon-fw icon-settings"></i> {{ t('App Settings') }}</a></li>
-  <li class="{% if current == 'security'%}active{% endif %}"><a href="/admin/security"><i class="icon-fw icon-shield"></i> {{ t('security_settings') }}</a></li>
-  <li class="{% if current == 'markdown'%}active{% endif %}"><a href="/admin/markdown"><i class="icon-fw icon-note"></i> {{ t('Markdown Settings') }}</a></li>
-  <li class="{% if current == 'customize'%}active{% endif %}"><a href="/admin/customize"><i class="icon-fw icon-wrench"></i> {{ t('Customize') }}</a></li>
-  <li class="{% if current == 'importer'%}active{% endif %}"><a href="/admin/importer"><i class="icon-fw icon-cloud-upload"></i> {{ t('Import Data') }}</a></li>
-  <li class="{% if current == 'export'%}active{% endif %}"><a href="/admin/export"><i class="icon-fw icon-cloud-download"></i> {{ t('Export Archive Data') }}</a></li>
-  <li class="{% if current == 'notification'%}active{% endif %}"><a href="/admin/notification"><i class="icon-fw icon-bell"></i> {{ t('Notification Settings') }}</a></li>
-  <li class="{% if current == 'user' || current == 'external-account' %}active{% endif %}"><a href="/admin/users"><i class="icon-fw icon-user"></i> {{ t('User_Management') }}</a></li>
-  <li class="{% if current == 'user-group'%}active{% endif %}"><a href="/admin/user-groups"><i class="icon-fw icon-people"></i> {{ t('UserGroup Management') }}</a></li>
-  <li class="{% if current == 'search'%}active{% endif %}"><a href="/admin/search"><i class="icon-fw icon-magnifier"></i> {{ t('Full Text Search Management') }}</a></li>
-</ul>

+ 0 - 16
src/server/views/admin/widget/theme-colorbox.html

@@ -1,16 +0,0 @@
-<div id="theme-option-{{name}}" class="theme-option-container d-flex flex-column align-items-center {% if name === settingForm['customize:theme'] %}active{% endif %}">
-  <a class="m-0 {{name}} theme-button"
-    id="{{name}}"
-    {% if 'kibela' !== settingForm['customize:layout'] %}onclick="selectTheme('{{name}}')"{% endif %}
-    data-theme="{{ webpack_asset('styles/theme-' + name + '.css') }}">
-
-    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
-      <g>
-        <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill="{{bg}}"></path>
-        <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill="{{topbar}}"></path>
-        <path d="M 44 15 L65 15 L65 65 L44 65 L44 15 Z" fill="{{theme}}"></path>
-      </g>
-    </svg>
-  </a>
-  <span class="theme-option-name"><b>{{name}}</b></span>
-</div>

+ 4 - 0
src/server/views/layout/admin.html

@@ -8,6 +8,10 @@
 <script src="{{ webpack_asset('js/admin.js') }}" defer></script>
 {% endblock %}
 
+{% block content_main %}
+  <div class="col-md-3" id="admin-navigation"></div>
+{% endblock content_main %}
+
 {# disable custom script in admin page #}
 {% block custom_script %}
 {% endblock %}

+ 1 - 1
src/server/views/widget/passport/ldap-association-tester.html

@@ -79,7 +79,7 @@
           // add logs
           if ('true' === '{{showLog}}') {
             if (data.err) {
-              addLogs($id, data.err);
+              addLogs($id, JSON.stringify(data.err, null, 2));
             }
             if (data.ldapConfiguration) {
               const prettified = JSON.stringify(data.ldapConfiguration.server, undefined, 4);