Ver Fonte

Merge branch 'reactify-admin/CustomizePage' into create-highlight-setting-frontside

# Conflicts:
#	resource/locales/en-US/translation.json
#	resource/locales/ja/translation.json
#	src/client/js/components/Admin/Customize/Customize.jsx
#	src/client/js/services/AdminCustomizeContainer.js
itizawa há 6 anos atrás
pai
commit
4e3776ab8c

+ 12 - 0
resource/locales/en-US/translation.json

@@ -682,6 +682,8 @@
     "update_behavior_success": "Succeeded to update behavior",
     "update_behavior_success": "Succeeded to update behavior",
     "update_function_success": "Succeeded to update function",
     "update_function_success": "Succeeded to update function",
     "update_highlight_success": "Succeeded to update code highlight",
     "update_highlight_success": "Succeeded to update code highlight",
+    "update_customCss_success": "Succeeded to update customize css",
+    "update_script_success": "Succeeded to update custom script",
     "layout_description":{
     "layout_description":{
       "growi_title":"Simple and Clear",
       "growi_title":"Simple and Clear",
       "growi_text1":"Full screen layout and thin margins/paddings",
       "growi_text1":"Full screen layout and thin margins/paddings",
@@ -695,6 +697,16 @@
       "crowi_text1":"Collapsible Sidebar",
       "crowi_text1":"Collapsible Sidebar",
       "crowi_text2":"Show and post comments in Sidebar",
       "crowi_text2":"Show and post comments in Sidebar",
       "crowi_text3":"Collapsible Table-of-contents"
       "crowi_text3":"Collapsible Table-of-contents"
+    },
+    "behavior_description":{
+      "growi_text1":"Both of <code>/page</code> and <code>/page/</code> shows the same page。",
+      "growi_text2":"<code>/nonexistent_page</code> shows editing form",
+      "growi_text3":"All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b>",
+      "crowi_text1":"<code>/page</code> shows the page",
+      "crowi_text2":"<code>/page/</code> shows the list of sub pages",
+      "crowi_text3":"If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown",
+      "crowi_text4":"<code>/nonexistent_page</code> shows editing form<",
+      "crowi_text5":"<code>/nonexistent_page/</code> the list of sub pages"
     }
     }
   },
   },
 
 

+ 12 - 0
resource/locales/ja/translation.json

@@ -666,6 +666,8 @@
     "update_behavior_success": "動作を更新しました",
     "update_behavior_success": "動作を更新しました",
     "update_function_success": "機能を更新しました",
     "update_function_success": "機能を更新しました",
     "update_highlight_success": "コードハイライトを更新しました",
     "update_highlight_success": "コードハイライトを更新しました",
+    "update_customCss_success": "カスタムCSSを更新しました",
+    "update_script_success": "カスタムスクリプトを更新しました",
     "layout_description":{
     "layout_description":{
       "growi_title":"シンプル・明瞭",
       "growi_title":"シンプル・明瞭",
       "growi_text1":"全画面レイアウトで、余白は少なくなります。",
       "growi_text1":"全画面レイアウトで、余白は少なくなります。",
@@ -679,6 +681,16 @@
       "crowi_text1":"サイドバーを開くと情報が表示されます。",
       "crowi_text1":"サイドバーを開くと情報が表示されます。",
       "crowi_text2":"コメントはサイドバーに表示されます。",
       "crowi_text2":"コメントはサイドバーに表示されます。",
       "crowi_text3":"ページ情報はサイドバーに表示されます。"
       "crowi_text3":"ページ情報はサイドバーに表示されます。"
+    },
+    "behavior_description":{
+      "growi_text1":"<code>/page</code>と<code>/page/</code>どちらのパスも同じページを表示します。",
+      "growi_text2":"<code>/nonexistent_page</code> では編集フォームを表示します",
+      "growi_text3":"<b>GROWI Enhanced Layout</b>では全てのページが配下のページリストを表示します",
+      "crowi_text1":"<code>/page</code> ではページを表示します。",
+      "crowi_text2":"<code>/page/</code> では配下のページを表示します。",
+      "crowi_text3":"<code>/page/</code>がポータルに適応している場合、ポータルページと配下のページリストを表示します。",
+      "crowi_text4":"<code>/nonexistent_page</code> では編集フォームを表示します",
+      "crowi_text5":"<code>/nonexistent_page</code> では配下のページリストを表示します。"
     }
     }
   },
   },
 
 

+ 0 - 22
src/client/js/app.jsx

@@ -35,8 +35,6 @@ import UserPictureList from './components/User/UserPictureList';
 import TableOfContents from './components/TableOfContents';
 import TableOfContents from './components/TableOfContents';
 
 
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
-import CustomCssEditor from './components/Admin/CustomCssEditor';
-import CustomScriptEditor from './components/Admin/CustomScriptEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import CustomHeaderEditor from './components/Admin/CustomHeaderEditor';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import MarkdownSetting from './components/Admin/MarkdownSetting/MarkDownSetting';
 import UserManagement from './components/Admin/UserManagement';
 import UserManagement from './components/Admin/UserManagement';
@@ -210,26 +208,6 @@ if (adminMarkDownSettingElem != null) {
   );
   );
 }
 }
 
 
-const customCssEditorElem = document.getElementById('custom-css-editor');
-if (customCssEditorElem != null) {
-  // get input[type=hidden] element
-  const customCssInputElem = document.getElementById('inputCustomCss');
-
-  ReactDOM.render(
-    <CustomCssEditor inputElem={customCssInputElem} />,
-    customCssEditorElem,
-  );
-}
-const customScriptEditorElem = document.getElementById('custom-script-editor');
-if (customScriptEditorElem != null) {
-  // get input[type=hidden] element
-  const customScriptInputElem = document.getElementById('inputCustomScript');
-
-  ReactDOM.render(
-    <CustomScriptEditor inputElem={customScriptInputElem} />,
-    customScriptEditorElem,
-  );
-}
 const customHeaderEditorElem = document.getElementById('custom-header-editor');
 const customHeaderEditorElem = document.getElementById('custom-header-editor');
 if (customHeaderEditorElem != null) {
 if (customHeaderEditorElem != null) {
   // get input[type=hidden] element
   // get input[type=hidden] element

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

@@ -16,12 +16,10 @@ require('jquery-ui/ui/widgets/resizable');
 export default class CustomCssEditor extends React.Component {
 export default class CustomCssEditor extends React.Component {
 
 
   render() {
   render() {
-    // get initial value from inputElem
-    const value = this.props.inputElem.value;
 
 
     return (
     return (
       <CodeMirror
       <CodeMirror
-        value={value}
+        value={this.props.value}
         autoFocus
         autoFocus
         options={{
         options={{
           mode: 'css',
           mode: 'css',
@@ -43,7 +41,7 @@ export default class CustomCssEditor extends React.Component {
           });
           });
         }}
         }}
         onChange={(editor, data, value) => {
         onChange={(editor, data, value) => {
-          this.props.inputElem.value = value;
+          this.props.onChange(value);
         }}
         }}
       />
       />
     );
     );
@@ -52,5 +50,6 @@ export default class CustomCssEditor extends React.Component {
 }
 }
 
 
 CustomCssEditor.propTypes = {
 CustomCssEditor.propTypes = {
-  inputElem: PropTypes.object.isRequired,
+  value: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
 };
 };

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

@@ -16,12 +16,10 @@ require('jquery-ui/ui/widgets/resizable');
 export default class CustomScriptEditor extends React.Component {
 export default class CustomScriptEditor extends React.Component {
 
 
   render() {
   render() {
-    // get initial value from inputElem
-    const value = this.props.inputElem.value;
 
 
     return (
     return (
       <CodeMirror
       <CodeMirror
-        value={value}
+        value={this.props.value}
         autoFocus
         autoFocus
         options={{
         options={{
           mode: 'javascript',
           mode: 'javascript',
@@ -43,7 +41,7 @@ export default class CustomScriptEditor extends React.Component {
           });
           });
         }}
         }}
         onChange={(editor, data, value) => {
         onChange={(editor, data, value) => {
-          this.props.inputElem.value = value;
+          this.props.onChange(value);
         }}
         }}
       />
       />
     );
     );
@@ -52,5 +50,6 @@ export default class CustomScriptEditor extends React.Component {
 }
 }
 
 
 CustomScriptEditor.propTypes = {
 CustomScriptEditor.propTypes = {
-  inputElem: PropTypes.object.isRequired,
+  value: PropTypes.string.isRequired,
+  onChange: PropTypes.func.isRequired,
 };
 };

+ 8 - 4
src/client/js/components/Admin/Customize/Customize.jsx

@@ -10,6 +10,8 @@ import CustomizeLayoutSetting from './CustomizeLayoutSetting';
 import CustomizeBehaviorSetting from './CustomizeBehaviorSetting';
 import CustomizeBehaviorSetting from './CustomizeBehaviorSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
+import CustomizeCssSetting from './CustomizeCssSetting';
+import CustomizeScriptSetting from './CustomizeScriptSetting';
 
 
 class Customize extends React.Component {
 class Customize extends React.Component {
 
 
@@ -32,10 +34,12 @@ class Customize extends React.Component {
         </div>
         </div>
         <legend>{t('customize_page.custom_title')}</legend>
         <legend>{t('customize_page.custom_title')}</legend>
         {/* カスタムタイトルフォームの react componentをここで呼ぶ(GW-278) */}
         {/* カスタムタイトルフォームの react componentをここで呼ぶ(GW-278) */}
-        <legend>{t('customize_page.Custom CSS')}</legend>
-        {/* カスタムCSSフォームの react componentをここで呼ぶ(GW-279) */}
-        <legend>{t('customize_page.Custom script')}</legend>
-        {/* カスタムスクリプトフォームの react componentをここで呼ぶ(GW-280) */}
+        <div className="my-3">
+          <CustomizeCssSetting />
+        </div>
+        <div className="my-3">
+          <CustomizeScriptSetting />
+        </div>
       </Fragment>
       </Fragment>
     );
     );
   }
   }

+ 10 - 11
src/client/js/components/Admin/Customize/CustomizeBehaviorSetting.jsx

@@ -1,3 +1,4 @@
+/* eslint-disable react/no-danger */
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
@@ -48,13 +49,12 @@ class CustomizeBehaviorSetting extends React.Component {
               behaviorType="growi"
               behaviorType="growi"
               isSelected={adminCustomizeContainer.state.currentBehavior === 'growi'}
               isSelected={adminCustomizeContainer.state.currentBehavior === 'growi'}
               onSelected={() => adminCustomizeContainer.switchBehaviorType('growi')}
               onSelected={() => adminCustomizeContainer.switchBehaviorType('growi')}
-              labelHtml='GROWI Simplified Behavior <small class="text-success">(Recommended)</small>'
+              labelHtml={`GROWI Simplified Behavior <small class="text-success">${t('customize_page.recommended')}</small>`}
             >
             >
-              {/* TODO i18n */}
               <ul>
               <ul>
-                <li>Both of <code>/page</code> and <code>/page/</code> shows the same page</li>
-                <li><code>/nonexistent_page</code> shows editing form</li>
-                <li>All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.growi_text1') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.growi_text2') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.growi_text3') }} /></li>
               </ul>
               </ul>
             </CustomizeBehaviorOption>
             </CustomizeBehaviorOption>
           </div>
           </div>
@@ -66,15 +66,14 @@ class CustomizeBehaviorSetting extends React.Component {
               onSelected={() => adminCustomizeContainer.switchBehaviorType('crowi-plus')}
               onSelected={() => adminCustomizeContainer.switchBehaviorType('crowi-plus')}
               labelHtml="Crowi Classic Behavior"
               labelHtml="Crowi Classic Behavior"
             >
             >
-              {/* TODO i18n */}
               <ul>
               <ul>
-                <li><code>/page</code> shows the page</li>
-                <li><code>/page/</code> shows the list of sub pages</li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text1') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text2') }} /></li>
                 <ul>
                 <ul>
-                  <li>If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown</li>
+                  <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text3') }} /></li>
                 </ul>
                 </ul>
-                <li><code>/nonexistent_page</code> shows editing form</li>
-                <li><code>/nonexistent_page/</code> the list of sub pages</li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text4') }} /></li>
+                <li><span dangerouslySetInnerHTML={{ __html: t('customize_page.behavior_description.crowi_text5') }} /></li>
               </ul>
               </ul>
             </CustomizeBehaviorOption>
             </CustomizeBehaviorOption>
           </div>
           </div>

+ 82 - 0
src/client/js/components/Admin/Customize/CustomizeCssSetting.jsx

@@ -0,0 +1,82 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+
+import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import CustomCssEditor from '../CustomCssEditor';
+
+const logger = loggerFactory('growi:Customize');
+
+class CustomizeCssSetting extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async onClickSubmit() {
+    const { t, adminCustomizeContainer } = this.props;
+
+    try {
+      await adminCustomizeContainer.updateCustomizeCss();
+      toastSuccess(t('customize_page.update_customCss_success'));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  render() {
+    const { t, appContainer, adminCustomizeContainer } = this.props;
+
+    return (
+      <React.Fragment>
+        <h2>{t('customize_page.Custom CSS')}</h2>
+        <p className="well">
+          { t('customize_page.write_CSS') }<br />
+          { t('customize_page.reflect_change') }
+        </p>
+        <div className="form-group">
+          <div className="col-xs-12">
+            <CustomCssEditor
+              // The value passed must be immutable
+              value={appContainer.config.customizeCss}
+              onChange={(inputValue) => { adminCustomizeContainer.changeCustomCss(inputValue) }}
+            />
+          </div>
+          <div className="col-xs-12">
+            <p className="help-block text-right">
+              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
+              { t('customize_page.ctrl_space') }
+            </p>
+          </div>
+        </div>
+
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} />
+      </React.Fragment>
+    );
+  }
+
+}
+
+const CustomizeCssSettingWrapper = (props) => {
+  return createSubscribedElement(CustomizeCssSetting, props, [AppContainer, AdminCustomizeContainer]);
+};
+
+CustomizeCssSetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
+};
+
+export default withTranslation()(CustomizeCssSettingWrapper);

+ 113 - 0
src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx

@@ -0,0 +1,113 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import loggerFactory from '@alias/logger';
+
+import { createSubscribedElement } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AppContainer from '../../../services/AppContainer';
+
+import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+import CustomScriptEditor from '../CustomScriptEditor';
+
+const logger = loggerFactory('growi:customizeScript');
+
+class CustomizeScriptSetting extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.onClickSubmit = this.onClickSubmit.bind(this);
+  }
+
+  async onClickSubmit() {
+    const { t, adminCustomizeContainer } = this.props;
+
+    try {
+      await adminCustomizeContainer.updateCustomizeScript();
+      toastSuccess(t('customize_page.update_script_success'));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  }
+
+  getExampleCode() {
+    return `console.log($('.main-container'));
+    window.addEventListener('load', (event) => {
+      console.log('config: ', appContainer.config);
+    });
+    `;
+  }
+
+  render() {
+    const { t, appContainer, adminCustomizeContainer } = this.props;
+
+    return (
+      <React.Fragment>
+        <h2>{t('customize_page.Custom script')}</h2>
+        <p className="well">
+          { t('customize_page.write_java') }<br />
+          { t('customize_page.reflect_change') }
+        </p>
+
+        <div className="help-block">
+          Placeholders:<br />
+          (Available after <code>load</code> event)
+          <dl className="dl-horizontal">
+            <dt><code>$</code></dt>
+            <dd>jQuery instance</dd>
+            <dt><code>appContainer</code></dt>
+            <dd>GROWI App <a href="https://github.com/jamiebuilds/unstated">Unstated Container</a></dd>
+            <dt><code>growiRenderer</code></dt>
+            <dd>GROWI Renderer origin instance</dd>
+            <dt><code>growiPlugin</code></dt>
+            <dd>GROWI Plugin Manager instance</dd>
+            <dt><code>Crowi</code></dt>
+            <dd>Crowi legacy instance (jQuery based)</dd>
+          </dl>
+        </div>
+
+        <div className="help-block">
+          Examples:
+          <pre className="hljs"><code>{this.getExampleCode()}</code></pre>
+        </div>
+
+        <div className="form-group">
+          <div className="col-xs-12">
+            <CustomScriptEditor
+              // The value passed must be immutable
+              value={appContainer.config.customizeScript}
+              onChange={(inputValue) => { adminCustomizeContainer.changeCustomizeScript(inputValue) }}
+            />
+          </div>
+          <div className="col-xs-12">
+            <p className="help-block text-right">
+              <i className="fa fa-fw fa-keyboard-o" aria-hidden="true" />
+              { t('customize_page.ctrl_space') }
+            </p>
+          </div>
+        </div>
+
+        <AdminUpdateButtonRow onClick={this.onClickSubmit} />
+      </React.Fragment>
+    );
+  }
+
+}
+
+const CustomizeScriptSettingWrapper = (props) => {
+  return createSubscribedElement(CustomizeScriptSetting, props, [AppContainer, AdminCustomizeContainer]);
+};
+
+CustomizeScriptSetting.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
+};
+
+export default withTranslation()(CustomizeScriptSettingWrapper);

+ 37 - 0
src/client/js/services/AdminCustomizeContainer.js

@@ -21,6 +21,8 @@ export default class AdminCustomizeContainer extends Container {
       currentRecentCreatedLimit: appContainer.config.recentCreatedLimit,
       currentRecentCreatedLimit: appContainer.config.recentCreatedLimit,
       currentHighlightJsStyle: appContainer.config.highlightJsStyle,
       currentHighlightJsStyle: appContainer.config.highlightJsStyle,
       isHighlightJsStyleBorderEnabled: appContainer.config.highlightJsStyleBorder,
       isHighlightJsStyleBorderEnabled: appContainer.config.highlightJsStyleBorder,
+      currentCustomizeCss: appContainer.config.customizeCss,
+      currentCustomizeScript: appContainer.config.customizeScript,
     };
     };
 
 
   }
   }
@@ -99,6 +101,21 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ isHighlightJsStyleBorderEnabled: !this.state.isHighlightJsStyleBorderEnabled });
     this.setState({ isHighlightJsStyleBorderEnabled: !this.state.isHighlightJsStyleBorderEnabled });
   }
   }
 
 
+  /**
+   * Change custom css
+   */
+  changeCustomCss(inputValue) {
+    this.setState({ currentCustomizeCss: inputValue });
+  }
+
+  /**
+   * Change customize script
+   */
+  changeCustomizeScript(inpuValue) {
+    this.setState({ currentCustomizeScript: inpuValue });
+  }
+
+
   /**
   /**
    * Update layout
    * Update layout
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
@@ -151,4 +168,24 @@ export default class AdminCustomizeContainer extends Container {
     // TODO GW-515 create apiV3
     // TODO GW-515 create apiV3
   }
   }
 
 
+
+  /**
+   * Update customCss
+   * @memberOf AdminCustomizeContainer
+   * @return {string} Customize css
+   */
+  async updateCustomizeCss() {
+    // TODO GW-534 create apiV3
+  }
+
+  /**
+   * Update customize script
+   * @memberOf AdminCustomizeContainer
+   * @return {string} Customize scripts
+   */
+  async updateCustomizeScript() {
+    // TODO GW-538 create apiV3
+  }
+
+
 }
 }

+ 2 - 0
src/server/models/config.js

@@ -184,8 +184,10 @@ module.exports = function(crowi) {
       attrWhiteList: crowi.xssService.getAttrWhiteList(),
       attrWhiteList: crowi.xssService.getAttrWhiteList(),
       highlightJsStyle: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
       highlightJsStyle: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
       highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
       highlightJsStyleBorder: crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
+      customizeCss: crowi.configManager.getConfig('crowi', 'customize:css'),
       isSavedStatesOfTabChanges: crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
       isSavedStatesOfTabChanges: crowi.configManager.getConfig('crowi', 'customize:isSavedStatesOfTabChanges'),
       isEnabledAttachTitleHeader: crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       isEnabledAttachTitleHeader: crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
+      customizeScript: crowi.configManager.getConfig('crowi', 'customize:script'),
       hasSlackConfig: crowi.slackNotificationService.hasSlackConfig(),
       hasSlackConfig: crowi.slackNotificationService.hasSlackConfig(),
       env: {
       env: {
         PLANTUML_URI: env.PLANTUML_URI || null,
         PLANTUML_URI: env.PLANTUML_URI || null,