Forráskód Böngészése

Merge pull request #3473 from tats-u/indent-size-changable

Make it possible to change indent size / インデント幅を変更可能にする
Yuki Takei 5 éve
szülő
commit
9f873c1b59

+ 1 - 0
package.json

@@ -96,6 +96,7 @@
     "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
     "date-fns": "^2.0.0",
+    "detect-indent": "^6.0.0",
     "diff": "^4.0.1",
     "elasticsearch": "^16.0.0",
     "entities": "^2.0.0",

+ 8 - 0
resource/locales/en_US/admin/admin.json

@@ -78,6 +78,14 @@
       "enable_lineBreak_for_comment": "Enable line break in comment",
       "enable_lineBreak_for_comment_desc": "Convert line break in comment to<code>&lt;br&gt;</code>in HTML"
     },
+    "indent_header": "Indent setting",
+    "indent_desc": "You can change indent settings.",
+    "indent_options": {
+      "indentSize": "Default indent size",
+      "indentSize_desc": "You can change the default indent size for the Markdown editor",
+      "disallow_indent_change": "Disallow change of indent size by users",
+      "disallow_indent_change_desc": "You can force users to use a specific indent size."
+    },
     "presentation_header": "Presentation setting",
     "presentation_desc": "You can change presentation settings.",
     "presentation_options": {

+ 8 - 0
resource/locales/ja_JP/admin/admin.json

@@ -78,6 +78,14 @@
       "enable_lineBreak_for_comment": "コメント欄で Line Break を有効にする",
       "enable_lineBreak_for_comment_desc": "コメント中の改行を、HTML内で<code>&lt;br&gt;</code>として扱います"
     },
+    "indent_header": "インデント設定",
+    "indent_desc": "インデントの設定を変更できます。",
+    "indent_options": {
+      "indentSize": "既定のインデント幅",
+      "indentSize_desc": "マークダウンの既定のインデント幅を変更できます。",
+      "disallow_indent_change": "ユーザによるインデント幅変更を許可しない",
+      "disallow_indent_change_desc": "ユーザに単一のインデント幅使用を強制することができます。"
+    },
     "presentation_header": "プレゼンテーション設定",
     "presentation_desc": "プレゼンテーションの設定を変更できます。",
     "presentation_options": {

+ 9 - 1
resource/locales/zh_CN/admin/admin.json

@@ -77,7 +77,15 @@
 			"enable_lineBreak_for_comment": "注释中启用换行符",
 			"enable_lineBreak_for_comment_desc": "HTML中将注释中的换行符转换为<code>&lt;br&gt;</code>"
 		},
-		"presentation_header": "演示文稿设置",
+    "indent_header": "缩进设置",
+    "indent_desc": "您可以更改缩进设置。",
+    "indent_options": {
+      "indentSize": "默认的缩进值",
+      "indentSize_desc": "您可以更改Markdown编辑器的默认的缩进值。",
+      "disallow_indent_change": "不允许用户更改缩进值",
+      "disallow_indent_change_desc": "您可以不允许用户更改缩进值。"
+    },
+    "presentation_header": "演示文稿设置",
 		"presentation_desc": "您可以更改演示文稿设置。",
 		"presentation_options": {
 			"page_break_setting": "分页设置",

+ 115 - 0
src/client/js/components/Admin/MarkdownSetting/IndentForm.jsx

@@ -0,0 +1,115 @@
+/* eslint-disable react/no-danger */
+import React from 'react';
+
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
+import {
+  UncontrolledDropdown, DropdownToggle, DropdownMenu, DropdownItem,
+} from 'reactstrap';
+
+import { withUnstatedContainers } from '../../UnstatedUtils';
+import { toastSuccess, toastError } from '../../../util/apiNotification';
+
+import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
+import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
+
+const logger = loggerFactory('growi:importer');
+
+const IndentForm = (props) => {
+  const onClickSubmit = async(props) => {
+    const { t } = props;
+
+    try {
+      await props.adminMarkDownContainer.updateIndentSetting();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.indent_header') }));
+    }
+    catch (err) {
+      toastError(err);
+      logger.error(err);
+    }
+  };
+
+  const renderIndentSizeOption = (props) => {
+    const { t, adminMarkDownContainer } = props;
+    const { adminPreferredIndentSize } = adminMarkDownContainer.state;
+
+    return (
+      <div className="col">
+        <div>
+          <label htmlFor="adminPreferredIndentSize">{t('admin:markdown_setting.indent_options.indentSize')}</label>
+          <UncontrolledDropdown id="adminPreferredIndentSize">
+            <DropdownToggle caret className="col-3 col-sm-2 col-md-5 col-lg-5 col-xl-3 text-right">
+              <span className="float-left">
+                {adminPreferredIndentSize || 4}
+              </span>
+            </DropdownToggle>
+            <DropdownMenu className="dropdown-menu" role="menu">
+              {[2, 4].map((num) => {
+                return (
+                  <DropdownItem key={num} role="presentation" onClick={() => adminMarkDownContainer.setAdminPreferredIndentSize(num)}>
+                    <a role="menuitem">{num}</a>
+                  </DropdownItem>
+                );
+              })}
+            </DropdownMenu>
+          </UncontrolledDropdown>
+        </div>
+        <p className="form-text text-muted">
+          {t('admin:markdown_setting.indent_options.indentSize_desc')}
+        </p>
+      </div>
+    );
+  };
+
+  const renderIndentForceOption = (props) => {
+    const { t, adminMarkDownContainer } = props;
+    const { isIndentSizeForced } = adminMarkDownContainer.state;
+
+    const helpIndentInComment = { __html: t('admin:markdown_setting.indent_options.disallow_indent_change_desc') };
+
+    return (
+      <div className="col">
+        <div className="custom-control custom-checkbox custom-checkbox-success">
+          <input
+            type="checkbox"
+            className="custom-control-input"
+            id="isIndentSizeForced"
+            checked={isIndentSizeForced || false}
+            onChange={() => {
+              adminMarkDownContainer.setState({ isIndentSizeForced: !isIndentSizeForced });
+            }}
+          />
+          <label className="custom-control-label" htmlFor="isIndentSizeForced">
+            {t('admin:markdown_setting.indent_options.disallow_indent_change')}
+          </label>
+        </div>
+        <p className="form-text text-muted" dangerouslySetInnerHTML={helpIndentInComment} />
+      </div>
+    );
+  };
+
+  const { adminMarkDownContainer } = props;
+
+  return (
+    <React.Fragment>
+      <fieldset className="form-group row row-cols-1 row-cols-md-2 mx-3">
+        {renderIndentSizeOption(props)}
+        {renderIndentForceOption(props)}
+      </fieldset>
+      <AdminUpdateButtonRow onClick={() => onClickSubmit(props)} disabled={adminMarkDownContainer.state.retrieveError != null} />
+    </React.Fragment>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const IndentFormWrapper = withUnstatedContainers(IndentForm, [AdminMarkDownContainer]);
+
+IndentForm.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
+};
+
+export default withTranslation()(IndentFormWrapper);

+ 8 - 0
src/client/js/components/Admin/MarkdownSetting/MarkDownSettingContents.jsx

@@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
 import LineBreakForm from './LineBreakForm';
+import IndentForm from './IndentForm';
 import PresentationForm from './PresentationForm';
 import XssForm from './XssForm';
 
@@ -21,6 +22,13 @@ class MarkDownSettingContents extends React.Component {
         </Card>
         <LineBreakForm />
 
+        {/* Indent Setting */}
+        <h2 className="admin-setting-header">{t('admin:markdown_setting.indent_header')}</h2>
+        <Card className="card well my-3">
+          <CardBody className="px-0 py-2">{t('admin:markdown_setting.indent_desc') }</CardBody>
+        </Card>
+        <IndentForm />
+
         {/* Presentation Setting */}
         <h2 className="admin-setting-header">{ t('admin:markdown_setting.presentation_header') }</h2>
         <Card className="card well my-3">

+ 5 - 0
src/client/js/components/PageComment/CommentEditor.jsx

@@ -23,6 +23,7 @@ import CommentPreview from './CommentPreview';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import { CustomNavTab } from '../CustomNavigation/CustomNav';
 
+
 const navTabMapping = {
   comment_editor: {
     Icon: () => <i className="icon-settings" />,
@@ -314,6 +315,10 @@ class CommentEditor extends React.Component {
                 onUpload={this.uploadHandler}
                 onCtrlEnter={this.ctrlEnterHandler}
               />
+              {/*
+                Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
+                See a review comment in https://github.com/weseek/growi/pull/3473
+              */}
             </TabPane>
             <TabPane tabId="comment_preview">
               <div className="comment-form-preview">

+ 10 - 0
src/client/js/components/PageEditor.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
+import detectIndent from 'detect-indent';
 
 import { throttle, debounce } from 'throttle-debounce';
 import { envUtils } from 'growi-commons';
@@ -59,6 +60,15 @@ class PageEditor extends React.Component {
       this.setState({ markdown: value });
     }));
     this.saveDraftWithDebounce = debounce(800, this.saveDraft);
+
+    // Detect indent size from contents (only when users are allowed to change it)
+    // TODO: https://youtrack.weseek.co.jp/issue/GW-5368
+    if (!this.props.appContainer.config.isIndentSizeForced && this.state.markdown) {
+      const detectedIndent = detectIndent(this.state.markdown);
+      if (detectedIndent.type === 'space' && new Set([2, 4]).has(detectedIndent.amount)) {
+        this.props.editorContainer.setState({ indentSize: detectedIndent.amount });
+      }
+    }
   }
 
   componentWillMount() {

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

@@ -843,7 +843,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
             styleActiveLine: this.props.editorOptions.styleActiveLine,
             lineNumbers: this.props.lineNumbers,
             tabSize: 4,
-            indentUnit: 4,
+            indentUnit: this.props.indentSize,
             lineWrapping: true,
             autoRefresh: { force: true }, // force option is enabled by autorefresh.ext.js -- Yuki Takei
             autoCloseTags: true,

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

@@ -309,6 +309,7 @@ export default class Editor extends AbstractEditor {
                       // eslint-disable-next-line arrow-body-style
                       <CodeMirrorEditor
                         ref={(c) => { this.cmEditor = c }}
+                        indentSize={editorContainer.state.indentSize}
                         editorOptions={editorContainer.state.editorOptions}
                         onPasteFiles={this.pasteFilesHandler}
                         onDragEnter={this.dragEnterHandler}

+ 38 - 0
src/client/js/components/PageEditor/OptionsSelector.jsx

@@ -44,6 +44,7 @@ class OptionsSelector extends React.Component {
       emacs: 'Emacs',
       sublime: 'Sublime Text',
     };
+    this.typicalIndentSizes = [2, 4];
 
     this.onChangeTheme = this.onChangeTheme.bind(this);
     this.onChangeKeymapMode = this.onChangeKeymapMode.bind(this);
@@ -51,6 +52,7 @@ class OptionsSelector extends React.Component {
     this.onClickRenderMathJaxInRealtime = this.onClickRenderMathJaxInRealtime.bind(this);
     this.onClickMarkdownTableAutoFormatting = this.onClickMarkdownTableAutoFormatting.bind(this);
     this.onToggleConfigurationDropdown = this.onToggleConfigurationDropdown.bind(this);
+    this.onChangeIndentSize = this.onChangeIndentSize.bind(this);
   }
 
   onChangeTheme(newValue) {
@@ -113,6 +115,11 @@ class OptionsSelector extends React.Component {
     this.setState({ isCddMenuOpened: !this.state.isCddMenuOpened });
   }
 
+  onChangeIndentSize(newValue) {
+    const { editorContainer } = this.props;
+    editorContainer.setState({ indentSize: newValue });
+  }
+
   renderThemeSelector() {
     const { editorContainer } = this.props;
 
@@ -279,11 +286,42 @@ class OptionsSelector extends React.Component {
     );
   }
 
+  renderIndentSizeSelector() {
+    const { appContainer, editorContainer } = this.props;
+    const menuItems = this.typicalIndentSizes.map((indent) => {
+      return <button key={indent} className="dropdown-item" type="button" onClick={() => this.onChangeIndentSize(indent)}>{indent}</button>;
+    });
+    return (
+      <div className="input-group flex-nowrap">
+        <div className="input-group-prepend">
+          <span className="input-group-text" id="igt-indent">Indent</span>
+        </div>
+        <div className="input-group-append dropup">
+          <button
+            type="button"
+            className="btn btn-outline-secondary dropdown-toggle"
+            data-toggle="dropdown"
+            aria-haspopup="true"
+            aria-expanded="false"
+            aria-describedby="igt-indent"
+            disabled={appContainer.config.isIndentSizeForced}
+          >
+            {editorContainer.state.indentSize}
+          </button>
+          <div className="dropdown-menu" aria-labelledby="dropdownMenuLink">
+            {menuItems}
+          </div>
+        </div>
+      </div>
+    );
+  }
+
   render() {
     return (
       <div className="d-flex flex-row">
         <span>{this.renderThemeSelector()}</span>
         <span className="ml-2 ml-sm-4">{this.renderKeymapModeSelector()}</span>
+        <span className="ml-2 ml-sm-4">{this.renderIndentSizeSelector()}</span>
         <span className="ml-2 ml-sm-4">{this.renderConfigurationDropdown()}</span>
       </div>
     );

+ 22 - 0
src/client/js/services/AdminMarkDownContainer.js

@@ -18,6 +18,8 @@ export default class AdminMarkDownContainer extends Container {
       // set dummy value tile for using suspense
       isEnabledLinebreaks: this.dummyIsEnabledLinebreaks,
       isEnabledLinebreaksInComments: false,
+      adminPreferredIndentSize: 4,
+      isIndentSizeForced: true,
       pageBreakSeparator: 1,
       pageBreakCustomSeparator: '',
       isEnabledXss: false,
@@ -27,6 +29,7 @@ export default class AdminMarkDownContainer extends Container {
     };
 
     this.switchEnableXss = this.switchEnableXss.bind(this);
+    this.setAdminPreferredIndentSize = this.setAdminPreferredIndentSize.bind(this);
   }
 
   /**
@@ -46,6 +49,8 @@ export default class AdminMarkDownContainer extends Container {
     this.setState({
       isEnabledLinebreaks: markdownParams.isEnabledLinebreaks,
       isEnabledLinebreaksInComments: markdownParams.isEnabledLinebreaksInComments,
+      adminPreferredIndentSize: markdownParams.adminPreferredIndentSize,
+      isIndentSizeForced: markdownParams.isIndentSizeForced,
       pageBreakSeparator: markdownParams.pageBreakSeparator,
       pageBreakCustomSeparator: markdownParams.pageBreakCustomSeparator || '',
       isEnabledXss: markdownParams.isEnabledXss,
@@ -55,6 +60,10 @@ export default class AdminMarkDownContainer extends Container {
     });
   }
 
+  setAdminPreferredIndentSize(adminPreferredIndentSize) {
+    this.setState({ adminPreferredIndentSize });
+  }
+
   /**
    * Switch PageBreakSeparator
    */
@@ -92,6 +101,19 @@ export default class AdminMarkDownContainer extends Container {
     return response;
   }
 
+  /**
+   * Update
+   */
+  async updateIndentSetting() {
+
+    const response = await this.appContainer.apiv3.put('/markdown-setting/indent', {
+      adminPreferredIndentSize: this.state.adminPreferredIndentSize,
+      isIndentSizeForced: this.state.isIndentSizeForced,
+    });
+
+    return response;
+  }
+
   /**
    * Update Xss Setting
    */

+ 1 - 0
src/client/js/services/EditorContainer.js

@@ -35,6 +35,7 @@ export default class EditorContainer extends Container {
 
       editorOptions: {},
       previewOptions: {},
+      indentSize: this.appContainer.config.adminPreferredIndentSize || 4,
     };
 
     this.isSetBeforeunloadEventHandler = false;

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

@@ -137,6 +137,8 @@ module.exports = function(crowi) {
       'markdown:xss:attrWhiteList': [],
       'markdown:isEnabledLinebreaks': false,
       'markdown:isEnabledLinebreaksInComments': true,
+      'markdown:adminPreferredIndentSize': 4,
+      'markdown:isIndentSizeForced': true,
       'markdown:presentation:pageBreakSeparator': 1,
       'markdown:presentation:pageBreakCustomSeparator': undefined,
     };
@@ -196,6 +198,8 @@ module.exports = function(crowi) {
       themeType: crowi.configManager.getConfig('crowi', 'customize:theme'),
       isEnabledLinebreaks: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaksInComments: crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      adminPreferredIndentSize: crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+      isIndentSizeForced: crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       pageBreakSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakCustomSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),

+ 29 - 0
src/server/routes/apiv3/markdown-setting.js

@@ -15,6 +15,10 @@ const validator = {
     body('isEnabledLinebreaks').isBoolean(),
     body('isEnabledLinebreaksInComments').isBoolean(),
   ],
+  indent: [
+    body('adminPreferredIndentSize').isIn([2, 4]),
+    body('isIndentSizeForced').isBoolean(),
+  ],
   presentationSetting: [
     body('pageBreakSeparator').isInt().not().isEmpty(),
   ],
@@ -111,6 +115,8 @@ module.exports = (crowi) => {
     const markdownParams = {
       isEnabledLinebreaks: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaks'),
       isEnabledLinebreaksInComments: await crowi.configManager.getConfig('markdown', 'markdown:isEnabledLinebreaksInComments'),
+      adminPreferredIndentSize: await crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+      isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
       pageBreakSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakSeparator'),
       pageBreakCustomSeparator: await crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       isEnabledXss: await crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
@@ -168,6 +174,29 @@ module.exports = (crowi) => {
 
   });
 
+  router.put('/indent', loginRequiredStrictly, adminRequired, csrf, validator.indent, apiV3FormValidator, async(req, res) => {
+
+    const requestIndentParams = {
+      'markdown:adminPreferredIndentSize': req.body.adminPreferredIndentSize,
+      'markdown:isIndentSizeForced': req.body.isIndentSizeForced,
+    };
+
+    try {
+      await crowi.configManager.updateConfigsInTheSameNamespace('markdown', requestIndentParams);
+      const indentParams = {
+        adminPreferredIndentSize: await crowi.configManager.getConfig('markdown', 'markdown:adminPreferredIndentSize'),
+        isIndentSizeForced: await crowi.configManager.getConfig('markdown', 'markdown:isIndentSizeForced'),
+      };
+      return res.apiv3({ indentParams });
+    }
+    catch (err) {
+      const msg = 'Error occurred in updating indent';
+      logger.error('Error', err);
+      return res.apiv3Err(new ErrorV3(msg, 'update-indent-failed'));
+    }
+
+  });
+
   /**
    * @swagger
    *

+ 5 - 0
yarn.lock

@@ -4938,6 +4938,11 @@ detect-indent@^5.0.0:
   resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d"
   integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50=
 
+detect-indent@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd"
+  integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA==
+
 detect-libc@^1.0.2:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b"