Procházet zdrojové kódy

Merge branch 'master' into support/apply-bootstrap4

# Conflicts:
#	src/server/views/admin/markdown.html
itizawa před 6 roky
rodič
revize
cf6d0d53bf

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

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

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