Просмотр исходного кода

Merge branch 'master' into reactify/personal-settings

itizawa 6 лет назад
Родитель
Сommit
9080e8389d

+ 14 - 1
CHANGES.md

@@ -1,8 +1,21 @@
 # CHANGES
 
-## v3.6.8-RC
+## v3.6.9-RC
 
+*
+
+## v3.6.8
+
+* Improvement: Show page history side-by-side
 * Improvement: Optimize markdown rendering
+* Improvement: Reactify admin pages (Navigation)
+* Fix: Reply comments collapsed are broken
+* Support: Update libs
+    * cross-env
+    * mkdirp
+    * diff2html
+    * jest
+    * stylelint
 
 ## v3.6.7
 

+ 6 - 6
package.json

@@ -1,6 +1,6 @@
 {
   "name": "growi",
-  "version": "3.6.8-RC",
+  "version": "3.6.9-RC",
   "description": "Team collaboration software using markdown",
   "tags": [
     "wiki",
@@ -85,7 +85,7 @@
     "connect-mongo": "^3.0.0",
     "connect-redis": "^3.3.0",
     "cookie-parser": "^1.4.3",
-    "cross-env": "^6.0.3",
+    "cross-env": "^7.0.0",
     "csrf": "^3.1.0",
     "date-fns": "^2.0.0",
     "diff": "^4.0.1",
@@ -112,7 +112,7 @@
     "md5": "^2.2.1",
     "method-override": "^3.0.0",
     "migrate-mongo": "^7.0.1",
-    "mkdirp": "~0.5.1",
+    "mkdirp": "^1.0.3",
     "module-alias": "^2.0.6",
     "mongoose": "5.4.4",
     "mongoose-gridfs": "^1.2.2",
@@ -172,7 +172,7 @@
     "core-js": "=2.6.9",
     "css-loader": "^3.0.0",
     "csv-to-markdown-table": "^1.0.1",
-    "diff2html": "^2.3.3",
+    "diff2html": "^3.1.2",
     "eazy-logger": "^3.0.2",
     "eslint": "^6.0.1",
     "eslint-config-weseek": "^1.0.3",
@@ -184,7 +184,7 @@
     "hard-source-webpack-plugin": "^0.13.1",
     "i18next-browser-languagedetector": "^4.0.1",
     "imports-loader": "^0.8.0",
-    "jest": "^24.8.0",
+    "jest": "^25.1.0",
     "jquery-slimscroll": "^1.3.8",
     "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
@@ -230,7 +230,7 @@
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^2.0.3",
     "style-loader": "^1.0.0",
-    "stylelint": "^12.0.1",
+    "stylelint": "^13.2.0",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger-jsdoc": "^3.4.0",
     "swagger2openapi": "^5.3.1",

+ 3 - 1
resource/locales/en-US/admin/admin.json

@@ -126,7 +126,9 @@
       "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
       "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page",
       "stale_notification": "Display Notification on Stale Pages",
-      "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update."
+      "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
+      "show_all_reply_comments": "Show all reply comments",
+      "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted."
     },
     "code_highlight": "Code Highlight",
     "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",

+ 3 - 1
resource/locales/ja/admin/admin.json

@@ -126,7 +126,9 @@
       "recent_created__n_draft_num_desc": "最近作成したページと下書きの表示数",
       "recently_created_n_draft_num_desc": "ホーム画面の Recently Created での、1ページの表示数を設定します。",
       "stale_notification": "古いページに通知を表示する",
-      "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。"
+      "stale_notification_desc": "最後の更新から1年を超えるページへの通知を表示します。",
+      "show_all_reply_comments": "返信コメントを全て表示する",
+      "show_all_reply_comments_desc": "OFFの場合、最新2件のコメント以外が省略されます。"
     },
     "code_highlight": "コードハイライト",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",

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

@@ -4,6 +4,7 @@ import loggerFactory from '@alias/logger';
 import Xss from '@commons/service/xss';
 
 import HeaderSearchBox from './components/HeaderSearchBox';
+import PersonalDropdown from './components/Navbar/PersonalDropdown';
 import StaffCredit from './components/StaffCredit/StaffCredit';
 
 import AppContainer from './services/AppContainer';
@@ -37,6 +38,7 @@ appContainer.injectToWindow();
 const componentMappings = {
   'search-top': <HeaderSearchBox />,
   'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+  'personal-dropdown': <PersonalDropdown />,
 
   'staff-credit': <StaffCredit />,
 };

+ 15 - 0
src/client/js/components/Admin/Customize/CustomizeFunctionSetting.jsx

@@ -133,6 +133,21 @@ class CustomizeBehaviorSetting extends React.Component {
           </div>
         </div>
 
+        <div className="form-group row">
+          <div className="col-xs-offset-3 col-xs-6 text-left">
+            <CustomizeFunctionOption
+              optionId="isAllReplyShown"
+              label={t('admin:customize_setting.function_options.show_all_reply_comments')}
+              isChecked={adminCustomizeContainer.state.isAllReplyShown || false}
+              onChecked={() => { adminCustomizeContainer.switchIsAllReplyShown() }}
+            >
+              <p className="help-block">
+                {t('admin:customize_setting.function_options.show_all_reply_comments_desc')}
+              </p>
+            </CustomizeFunctionOption>
+          </div>
+        </div>
+
         <AdminUpdateButtonRow onClick={this.onClickSubmit} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </React.Fragment>
     );

+ 1 - 1
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -27,7 +27,7 @@ class XssForm extends React.Component {
 
     try {
       await this.props.adminMarkDownContainer.updateXssSetting();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.xss_desc') }));
+      toastSuccess(t('toaster.update_successed', { target: t('admin:markdown_setting.xss_header') }));
     }
     catch (err) {
       toastError(err);

+ 61 - 0
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -0,0 +1,61 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
+import UserPicture from '../User/UserPicture';
+
+const PersonalDropdown = (props) => {
+
+  const { t, appContainer } = props;
+  const username = appContainer.me;
+  const user = appContainer.findUser(username);
+
+  const logoutHandler = () => {
+    const { interceptorManager } = appContainer;
+
+    const context = {
+      user,
+      currentPagePath: decodeURIComponent(window.location.pathname),
+    };
+    interceptorManager.process('logout', context);
+
+    window.location.href = '/logout';
+  };
+
+  return (
+    <>
+      <a className="dropdown-toggle waves-effect waves-light" data-toggle="dropdown">
+        <UserPicture user={user} withoutLink />&nbsp;{user.name}
+      </a>
+      <ul className="dropdown-menu dropdown-menu-right">
+        <li><a href={`/user/${user.username}`}><i className="icon-fw icon-home"></i>{ t('Home') }</a></li>
+        <li><a href="/me"><i className="icon-fw icon-wrench"></i>{ t('User Settings') }</a></li>
+        <li role="separator" className="divider"></li>
+        <li><a href={`/user/${user.username}#user-draft-list`}><i className="icon-fw icon-docs"></i>{ t('List Drafts') }</a></li>
+        <li><a href="/trash"><i className="icon-fw icon-trash"></i>{ t('Deleted Pages') }</a></li>
+        <li role="separator" className="divider"></li>
+        <li><a role="button" onClick={logoutHandler}><i className="icon-fw icon-power"></i>{ t('Sign out') }</a></li>
+      </ul>
+    </>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PersonalDropdownWrapper = (props) => {
+  return createSubscribedElement(PersonalDropdown, props, [AppContainer]);
+};
+
+
+PersonalDropdown.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
+export default withTranslation()(PersonalDropdownWrapper);

+ 55 - 150
src/client/js/components/PageComment/Comment.jsx

@@ -3,10 +3,8 @@ import PropTypes from 'prop-types';
 
 import { format, formatDistanceStrict } from 'date-fns';
 
-import Button from 'react-bootstrap/es/Button';
 import Tooltip from 'react-bootstrap/es/Tooltip';
 import OverlayTrigger from 'react-bootstrap/es/OverlayTrigger';
-import Collapse from 'react-bootstrap/es/Collapse';
 
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
@@ -16,6 +14,7 @@ import RevisionBody from '../Page/RevisionBody';
 import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
 import CommentEditor from './CommentEditor';
+import CommentControl from './CommentControl';
 
 /**
  *
@@ -25,15 +24,14 @@ import CommentEditor from './CommentEditor';
  * @class Comment
  * @extends {React.Component}
  */
-class Comment extends React.Component {
+class Comment extends React.PureComponent {
 
   constructor(props) {
     super(props);
 
     this.state = {
       html: '',
-      isOlderRepliesShown: false,
-      showReEditorIds: new Set(),
+      isReEdit: false,
     };
 
     this.growiRenderer = this.props.appContainer.getRenderer('comment');
@@ -42,23 +40,39 @@ class Comment extends React.Component {
     this.isCurrentRevision = this.isCurrentRevision.bind(this);
     this.getRootClassName = this.getRootClassName.bind(this);
     this.getRevisionLabelClassName = this.getRevisionLabelClassName.bind(this);
+    this.editBtnClickedHandler = this.editBtnClickedHandler.bind(this);
     this.deleteBtnClickedHandler = this.deleteBtnClickedHandler.bind(this);
     this.renderText = this.renderText.bind(this);
     this.renderHtml = this.renderHtml.bind(this);
     this.commentButtonClickedHandler = this.commentButtonClickedHandler.bind(this);
   }
 
-  componentWillMount() {
-    this.renderHtml(this.props.comment.comment);
+
+  initCurrentRenderingContext() {
+    this.currentRenderingContext = {
+      markdown: this.props.comment.comment,
+    };
   }
 
-  componentWillReceiveProps(nextProps) {
-    this.renderHtml(nextProps.comment.comment);
+  componentDidMount() {
+    this.initCurrentRenderingContext();
+    this.renderHtml();
   }
 
-  // not used
-  setMarkdown(markdown) {
-    this.renderHtml(markdown);
+  componentDidUpdate(prevProps) {
+    const { comment: prevComment } = prevProps;
+    const { comment } = this.props;
+
+    // render only when props.markdown is updated
+    if (comment !== prevComment) {
+      this.initCurrentRenderingContext();
+      this.renderHtml();
+      return;
+    }
+
+    const { interceptorManager } = this.props.appContainer;
+
+    interceptorManager.process('postRenderCommentHtml', this.currentRenderingContext);
   }
 
   checkPermissionToControlComment() {
@@ -99,18 +113,12 @@ class Comment extends React.Component {
       this.isCurrentRevision() ? 'label-primary' : 'label-default'}`;
   }
 
-  editBtnClickedHandler(commentId) {
-    const ids = this.state.showReEditorIds.add(commentId);
-    this.setState({ showReEditorIds: ids });
+  editBtnClickedHandler() {
+    this.setState({ isReEdit: !this.state.isReEdit });
   }
 
-  commentButtonClickedHandler(commentId) {
-    this.setState((prevState) => {
-      prevState.showReEditorIds.delete(commentId);
-      return {
-        showReEditorIds: prevState.showReEditorIds,
-      };
-    });
+  commentButtonClickedHandler() {
+    this.editBtnClickedHandler();
   }
 
   deleteBtnClickedHandler() {
@@ -134,120 +142,23 @@ class Comment extends React.Component {
     );
   }
 
-  toggleOlderReplies() {
-    this.setState((prevState) => {
-      return {
-        showOlderReplies: !prevState.showOlderReplies,
-      };
-    });
-  }
-
-  renderHtml(markdown) {
-    const context = {
-      markdown,
-    };
-
-    const growiRenderer = this.props.growiRenderer;
-    const interceptorManager = this.props.appContainer.interceptorManager;
-    interceptorManager.process('preRenderComment', 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('preRenderCommentHtml', context) })
-      .then(() => {
-        this.setState({ html: context.parsedHTML });
-      })
-      // process interceptors for post rendering
-      .then(() => { return interceptorManager.process('postRenderCommentHtml', context) });
-
-  }
-
-  renderReply(reply) {
-    return (
-      <div key={reply._id} className="page-comment-reply">
-        <CommentWrapper
-          comment={reply}
-          deleteBtnClicked={this.props.deleteBtnClicked}
-          growiRenderer={this.props.growiRenderer}
-        />
-      </div>
-    );
-  }
-
-  renderReplies() {
-    const layoutType = this.props.appContainer.getConfig().layoutType;
-    const isBaloonStyle = layoutType.match(/crowi-plus|growi|kibela/);
-
-    let replyList = this.props.replyList;
-    if (!isBaloonStyle) {
-      replyList = replyList.slice().reverse();
-    }
-
-    const areThereHiddenReplies = replyList.length > 2;
-
-    const { isOlderRepliesShown } = this.state;
-    const toggleButtonIconName = isOlderRepliesShown ? 'icon-arrow-up' : 'icon-options-vertical';
-    const toggleButtonIcon = <i className={`icon-fw ${toggleButtonIconName}`}></i>;
-    const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
-    const toggleButton = (
-      <Button
-        bsStyle="link"
-        className="page-comments-list-toggle-older"
-        onClick={() => { this.setState({ isOlderRepliesShown: !isOlderRepliesShown }) }}
-      >
-        {toggleButtonIcon} {toggleButtonLabel}
-      </Button>
-    );
-
-    const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
-    const hiddenReplies = replyList.slice(0, replyList.length - 2);
-
-    const hiddenElements = hiddenReplies.map((reply) => {
-      return this.renderReply(reply);
-    });
-
-    const shownElements = shownReplies.map((reply) => {
-      return this.renderReply(reply);
-    });
-
-    return (
-      <React.Fragment>
-        { areThereHiddenReplies && (
-          <div className="page-comments-hidden-replies">
-            <Collapse in={this.state.isOlderRepliesShown}>
-              <div>{hiddenElements}</div>
-            </Collapse>
-            <div className="text-center">{toggleButton}</div>
-          </div>
-        ) }
+  async renderHtml() {
 
-        {shownElements}
-      </React.Fragment>
-    );
-  }
+    const { growiRenderer, appContainer } = this.props;
+    const { interceptorManager } = appContainer;
+    const context = this.currentRenderingContext;
 
-  renderCommentControl(comment) {
-    return (
-      <div className="page-comment-control">
-        <button type="button" className="btn btn-link p-2" onClick={() => { this.editBtnClickedHandler(comment._id) }}>
-          <i className="ti-pencil"></i>
-        </button>
-        <button type="button" className="btn btn-link p-2 mr-2" onClick={this.deleteBtnClickedHandler}>
-          <i className="ti-close"></i>
-        </button>
-      </div>
-    );
+    await interceptorManager.process('preRenderComment', context);
+    await interceptorManager.process('prePreProcess', context);
+    context.markdown = await growiRenderer.preProcess(context.markdown);
+    await interceptorManager.process('postPreProcess', context);
+    context.parsedHTML = await growiRenderer.process(context.markdown);
+    await interceptorManager.process('prePostProcess', context);
+    context.parsedHTML = await growiRenderer.postProcess(context.parsedHTML);
+    await interceptorManager.process('postPostProcess', context);
+    await interceptorManager.process('preRenderCommentHtml', context);
+    this.setState({ html: context.parsedHTML });
+    await interceptorManager.process('postRenderCommentHtml', context);
   }
 
   render() {
@@ -259,8 +170,6 @@ class Comment extends React.Component {
     const updatedAt = new Date(comment.updatedAt);
     const isEdited = createdAt < updatedAt;
 
-    const showReEditor = this.state.showReEditorIds.has(commentId);
-
     const rootClassName = this.getRootClassName(comment);
     const commentDate = formatDistanceStrict(createdAt, new Date());
     const commentBody = isMarkdown ? this.renderRevisionBody() : this.renderText(comment.comment);
@@ -284,7 +193,7 @@ class Comment extends React.Component {
     return (
       <React.Fragment>
 
-        {showReEditor ? (
+        {this.state.isReEdit ? (
           <CommentEditor
             growiRenderer={this.growiRenderer}
             currentCommentId={commentId}
@@ -305,19 +214,19 @@ class Comment extends React.Component {
                 <OverlayTrigger overlay={commentDateTooltip} placement="bottom">
                   <span><a href={`#${commentId}`}>{commentDate}</a></span>
                 </OverlayTrigger>
-                { isEdited && (
-                  <OverlayTrigger overlay={editedDateTooltip} placement="bottom">
-                    <span>&nbsp;(edited)</span>
-                  </OverlayTrigger>
-                ) }
+                {isEdited && (
+                <OverlayTrigger overlay={editedDateTooltip} placement="bottom">
+                  <span>&nbsp;(edited)</span>
+                </OverlayTrigger>
+                  )}
                 <span className="ml-2"><a className={revisionLavelClassName} href={revHref}>{revFirst8Letters}</a></span>
               </div>
-              { this.checkPermissionToControlComment() && this.renderCommentControl(comment) }
+              {this.checkPermissionToControlComment()
+                  && <CommentControl onClickDeleteBtn={this.deleteBtnClickedHandler} onClickEditBtn={this.editBtnClickedHandler} />}
             </div>
           </div>
-        )
-      }
-        {this.renderReplies()}
+          )
+        }
 
       </React.Fragment>
     );
@@ -339,10 +248,6 @@ Comment.propTypes = {
   comment: PropTypes.object.isRequired,
   growiRenderer: PropTypes.object.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
-  replyList: PropTypes.array,
-};
-Comment.defaultProps = {
-  replyList: [],
 };
 
 export default CommentWrapper;

+ 24 - 0
src/client/js/components/PageComment/CommentControl.jsx

@@ -0,0 +1,24 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+
+const CommentControl = (props) => {
+  return (
+    <div className="page-comment-control">
+      <button type="button" className="btn btn-link p-2" onClick={props.onClickEditBtn}>
+        <i className="ti-pencil"></i>
+      </button>
+      <button type="button" className="btn btn-link p-2 mr-2" onClick={props.onClickDeleteBtn}>
+        <i className="ti-close"></i>
+      </button>
+    </div>
+  );
+};
+
+CommentControl.propTypes = {
+
+  onClickEditBtn: PropTypes.func.isRequired,
+  onClickDeleteBtn: PropTypes.func.isRequired,
+};
+
+export default CommentControl;

+ 124 - 0
src/client/js/components/PageComment/ReplayComments.jsx

@@ -0,0 +1,124 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import Button from 'react-bootstrap/es/Button';
+import Collapse from 'react-bootstrap/es/Collapse';
+
+import AppContainer from '../../services/AppContainer';
+import PageContainer from '../../services/PageContainer';
+
+import Comment from './Comment';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+
+class ReplayComments extends React.PureComponent {
+
+  constructor() {
+    super();
+
+    this.state = {
+      isOlderRepliesShown: false,
+    };
+
+    this.toggleIsOlderRepliesShown = this.toggleIsOlderRepliesShown.bind(this);
+  }
+
+  toggleIsOlderRepliesShown() {
+    this.setState({ isOlderRepliesShown: !this.state.isOlderRepliesShown });
+  }
+
+  renderReply(reply) {
+    return (
+      <div key={reply._id} className="page-comment-reply">
+        <Comment
+          comment={reply}
+          deleteBtnClicked={this.props.deleteBtnClicked}
+          growiRenderer={this.props.growiRenderer}
+        />
+      </div>
+    );
+  }
+
+  render() {
+
+    const layoutType = this.props.appContainer.getConfig().layoutType;
+    const isBaloonStyle = layoutType.match(/crowi-plus|growi|kibela/);
+
+    const isAllReplyShown = this.props.appContainer.getConfig().isAllReplyShown || false;
+
+    let replyList = this.props.replyList;
+    if (!isBaloonStyle) {
+      replyList = replyList.slice().reverse();
+    }
+
+    if (isAllReplyShown) {
+      return (
+        <React.Fragment>
+          {replyList.map((reply) => {
+            return this.renderReply(reply);
+          })}
+        </React.Fragment>
+      );
+    }
+
+    const areThereHiddenReplies = (replyList.length > 2);
+
+    const { isOlderRepliesShown } = this.state;
+    const toggleButtonIconName = isOlderRepliesShown ? 'icon-arrow-up' : 'icon-options-vertical';
+    const toggleButtonIcon = <i className={`icon-fw ${toggleButtonIconName}`}></i>;
+    const toggleButtonLabel = isOlderRepliesShown ? '' : 'more';
+
+    const shownReplies = replyList.slice(replyList.length - 2, replyList.length);
+    const hiddenReplies = replyList.slice(0, replyList.length - 2);
+
+    const hiddenElements = hiddenReplies.map((reply) => {
+      return this.renderReply(reply);
+    });
+
+    const shownElements = shownReplies.map((reply) => {
+      return this.renderReply(reply);
+    });
+
+    return (
+      <React.Fragment>
+        {areThereHiddenReplies && (
+          <div className="page-comments-hidden-replies">
+            <Collapse in={this.state.isOlderRepliesShown}>
+              <div>{hiddenElements}</div>
+            </Collapse>
+            <div className="text-center">
+              <Button
+                bsStyle="link"
+                className="page-comments-list-toggle-older"
+                onClick={this.toggleIsOlderRepliesShown}
+              >
+                {toggleButtonIcon} {toggleButtonLabel}
+              </Button>
+            </div>
+          </div>
+        )}
+        {shownElements}
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const ReplayCommentsWrapper = (props) => {
+  return createSubscribedElement(ReplayComments, props, [AppContainer, PageContainer]);
+};
+
+ReplayComments.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  growiRenderer: PropTypes.object.isRequired,
+  deleteBtnClicked: PropTypes.func.isRequired,
+  replyList: PropTypes.array,
+};
+
+export default ReplayCommentsWrapper;

+ 7 - 1
src/client/js/components/PageComments.jsx

@@ -14,6 +14,7 @@ import { createSubscribedElement } from './UnstatedUtils';
 import CommentEditor from './PageComment/CommentEditor';
 import Comment from './PageComment/Comment';
 import DeleteCommentModal from './PageComment/DeleteCommentModal';
+import ReplayComments from './PageComment/ReplayComments';
 
 
 /**
@@ -138,11 +139,16 @@ class PageComments extends React.Component {
       <div key={commentId} className={`mb-5 ${rootClassNames}`}>
         <Comment
           comment={comment}
-          editBtnClicked={this.confirmToEditComment}
           deleteBtnClicked={this.confirmToDeleteComment}
           growiRenderer={this.growiRenderer}
+        />
+        {replies.length !== 0 && (
+        <ReplayComments
           replyList={replies}
+          deleteBtnClicked={this.confirmToDeleteComment}
+          growiRenderer={this.growiRenderer}
         />
+        )}
         { !showEditor && isLoggedIn && (
           <div className="text-right">
             <Button

+ 6 - 2
src/client/js/components/PageHistory/RevisionDiff.jsx

@@ -2,7 +2,7 @@ import React from 'react';
 import PropTypes from 'prop-types';
 
 import { createPatch } from 'diff';
-import { Diff2Html } from 'diff2html';
+import { html } from 'diff2html';
 
 export default class RevisionDiff extends React.Component {
 
@@ -29,8 +29,12 @@ export default class RevisionDiff extends React.Component {
         previousText,
         currentRevision.body,
       );
+      const option = {
+        drawFileList: false,
+        outputFormat: 'side-by-side',
+      };
 
-      diffViewHTML = Diff2Html.getPrettyHtml(patch);
+      diffViewHTML = html(patch, option);
     }
 
     const diffView = { __html: diffViewHTML };

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

@@ -28,6 +28,7 @@ export default class AdminCustomizeContainer extends Container {
       isEnabledAttachTitleHeader: false,
       currentRecentCreatedLimit: 10,
       isEnabledStaleNotification: false,
+      isAllReplyShown: false,
       currentHighlightJsStyleId: '',
       isHighlightJsStyleBorderEnabled: false,
       currentCustomizeTitle: '',
@@ -76,6 +77,7 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledAttachTitleHeader: customizeParams.isEnabledAttachTitleHeader,
         currentRecentCreatedLimit: customizeParams.recentCreatedLimit,
         isEnabledStaleNotification: customizeParams.isEnabledStaleNotification,
+        isAllReplyShown: customizeParams.isAllReplyShown,
         currentHighlightJsStyleId: customizeParams.styleName,
         isHighlightJsStyleBorderEnabled: customizeParams.styleBorder,
         currentCustomizeTitle: customizeParams.customizeTitle,
@@ -159,6 +161,13 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ isEnabledStaleNotification:  !this.state.isEnabledStaleNotification });
   }
 
+  /**
+   * Switch isAllReplyShown
+   */
+  switchIsAllReplyShown() {
+    this.setState({ isAllReplyShown: !this.state.isAllReplyShown });
+  }
+
   /**
    * Switch highlightJsStyle
    */
@@ -289,6 +298,7 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledAttachTitleHeader: this.state.isEnabledAttachTitleHeader,
         recentCreatedLimit: this.state.currentRecentCreatedLimit,
         isEnabledStaleNotification: this.state.isEnabledStaleNotification,
+        isAllReplyShown: this.state.isAllReplyShown,
       });
       const { customizedParams } = response.data;
       this.setState({
@@ -297,6 +307,7 @@ export default class AdminCustomizeContainer extends Container {
         isEnabledAttachTitleHeader: customizedParams.isEnabledAttachTitleHeader,
         recentCreatedLimit: customizedParams.currentRecentCreatedLimit,
         isEnabledStaleNotification: customizedParams.isEnabledStaleNotification,
+        isAllReplyShown: customizedParams.isAllReplyShown,
       });
     }
     catch (err) {

+ 8 - 1
src/client/styles/scss/_comment.scss

@@ -1,11 +1,17 @@
 .main-container {
   .page-comment-main {
+    pointer-events: auto;
+
     // delete button
     .page-comment-control {
       position: absolute;
       top: 0;
       right: 0;
-      display: none; // default hidden
+      visibility: hidden;
+    }
+
+    &:hover > .page-comment-control {
+      visibility: visible;
     }
   }
 
@@ -24,6 +30,7 @@
 .page-comment {
   padding-top: 50px;
   margin-top: -50px;
+  pointer-events: none;
 }
 
 .main-container {

+ 0 - 5
src/client/styles/scss/_comment_growi.scss

@@ -103,11 +103,6 @@
     border-left: none;
   }
 
-  // show when hover
-  .page-comment-main:hover > .page-comment-control {
-    display: block;
-  }
-
   // display cheatsheet for comment form only
   .comment-form {
     .editor-cheatsheet {

+ 0 - 5
src/client/styles/scss/_comment_kibela.scss

@@ -105,11 +105,6 @@
     border-left: none;
   }
 
-  // show when hover
-  .page-comment-main:hover > .page-comment-control {
-    display: block;
-  }
-
   // display cheatsheet for comment form only
   .comment-form {
     .editor-cheatsheet {

+ 0 - 5
src/client/styles/scss/_layout_crowi_sidebar.scss

@@ -144,11 +144,6 @@
               display: none; // default hidden
             }
           }
-
-          // show controls when hover
-          .page-comment-main:hover > .page-comment-control {
-            display: block;
-          }
         }
       }
     }

+ 1 - 4
src/client/styles/scss/_page.scss

@@ -1,5 +1,5 @@
 // import diff2html styles
-@import '~diff2html/dist/diff2html.css';
+@import '~diff2html/bundles/css/diff2html.min.css';
 
 .main-container {
   // padding controll of .header-wrap and .content-main are moved to _layout and _form
@@ -8,7 +8,6 @@
    * header
    */
   header {
-
     // the container of h1
     div.title-container {
       padding-right: 5px;
@@ -39,7 +38,6 @@
 
     // change button opacity
     &:hover {
-
       .btn.btn-copy,
       .btn-copy-link,
       .btn.btn-edit,
@@ -116,7 +114,6 @@
 .main-container .main .content-main .revision-history {
   .revision-history-list {
     .revision-history-outer {
-
       // add border-top except of first element
       &:not(:first-of-type) {
         border-top: 1px solid $border;

+ 5 - 3
src/linter-checker/test.js

@@ -1,13 +1,15 @@
 /*
  * VSCode の Eslint 設定チェック方法
  *
- * 1. VSCode で以下のエラーが表示されていることを確認
+ * 1. .eslilntignore ファイル中の `/src/linter-checker/**` 行を消す
+ * 
+ * 2. VSCode で以下のエラーが表示されていることを確認
  *   - constructor で eslint(space-before-blocks)
  *   - ファイル末尾の ";" で eslint(eol-last)
  *
- * 2. VSCode で上書き保存
+ * 3. VSCode で上書き保存
  *
- * 3. 以下のように整形され、全てのエラーが消えていることを確認
+ * 4. 以下のように整形され、全てのエラーが消えていることを確認
  *   - "constructor() {" のように間にスペースが入る
  *   - ファイル末尾に空行が入る
  *

+ 5 - 3
src/linter-checker/test.scss

@@ -1,13 +1,15 @@
 /*
  * VSCode の Stylelint 設定チェック方法
  *
- * 1. VSCode で以下のエラーが表示されていることを確認
+ * 1. .stylelintrc.json ファイル中の `src/linter-checker/test.scss` 行を削除
+ * 
+ * 2. VSCode で以下のエラーが表示されていることを確認
  *   - color で stylelint(order/properties-order)
  *   - ul で stylelint(selector-combinator-space-after)
  *
- * 2. VSCode で上書き保存
+ * 3. VSCode で上書き保存
  *
- * 3. 以下のように整形され、全てのエラーが消えていることを確認
+ * 4. 以下のように整形され、全てのエラーが消えていることを確認
  *   - color が background の上の行にくる
  *   - ul と li の間にスペースが入る
  *

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

@@ -109,6 +109,7 @@ module.exports = function(crowi) {
       'customize:isEnabledAttachTitleHeader' : false,
       'customize:showRecentCreatedNumber' : 10,
       'customize:isEnabledStaleNotification': false,
+      'customize:isAllReplyShown': false,
 
       'importer:esa:team_name': undefined,
       'importer:esa:access_token': undefined,
@@ -189,6 +190,7 @@ module.exports = function(crowi) {
       pageBreakCustomSeparator: crowi.configManager.getConfig('markdown', 'markdown:presentation:pageBreakCustomSeparator'),
       isEnabledXssPrevention: crowi.configManager.getConfig('markdown', 'markdown:xss:isEnabledPrevention'),
       isEnabledTimeline: crowi.configManager.getConfig('crowi', 'customize:isEnabledTimeline'),
+      isAllReplyShown: crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
       xssOption: crowi.configManager.getConfig('markdown', 'markdown:xss:option'),
       tagWhiteList: crowi.xssService.getTagWhiteList(),
       attrWhiteList: crowi.xssService.getAttrWhiteList(),

+ 6 - 0
src/server/routes/apiv3/customize-setting.js

@@ -49,6 +49,8 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *            type: number
  *          isEnabledStaleNotification:
  *            type: boolean
+ *          isAllReplyShown:
+ *            type: boolean
  *      CustomizeHighlight:
  *        description: CustomizeHighlight
  *        type: object
@@ -112,6 +114,7 @@ module.exports = (crowi) => {
       body('isEnabledAttachTitleHeader').isBoolean(),
       body('recentCreatedLimit').isInt().isInt({ min: 1, max: 1000 }),
       body('isEnabledStaleNotification').isBoolean(),
+      body('isAllReplyShown').isBoolean(),
     ],
     customizeTitle: [
       body('customizeTitle').isString(),
@@ -164,6 +167,7 @@ module.exports = (crowi) => {
       isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
       recentCreatedLimit: await crowi.configManager.getConfig('crowi', 'customize:showRecentCreatedNumber'),
       isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
+      isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
       styleName: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyle'),
       styleBorder: await crowi.configManager.getConfig('crowi', 'customize:highlightJsStyleBorder'),
       customizeTitle: await crowi.configManager.getConfig('crowi', 'customize:title'),
@@ -329,6 +333,7 @@ module.exports = (crowi) => {
       'customize:isEnabledAttachTitleHeader': req.body.isEnabledAttachTitleHeader,
       'customize:showRecentCreatedNumber': req.body.recentCreatedLimit,
       'customize:isEnabledStaleNotification': req.body.isEnabledStaleNotification,
+      'customize:isAllReplyShown': req.body.isAllReplyShown,
     };
 
     try {
@@ -339,6 +344,7 @@ module.exports = (crowi) => {
         isEnabledAttachTitleHeader: await crowi.configManager.getConfig('crowi', 'customize:isEnabledAttachTitleHeader'),
         recentCreatedLimit: await crowi.configManager.getConfig('crowi', 'customize:showRecentCreatedNumber'),
         isEnabledStaleNotification: await crowi.configManager.getConfig('crowi', 'customize:isEnabledStaleNotification'),
+        isAllReplyShown: await crowi.configManager.getConfig('crowi', 'customize:isAllReplyShown'),
       };
       return res.apiv3({ customizedParams });
     }

+ 1 - 15
src/server/views/layout/layout.html

@@ -141,21 +141,7 @@
             <i class="icon-question"></i><span>{{ t('Help') }}</span><span class="text-muted small"><i class="icon-share-alt"></i></span>
           </a>
         </li>
-        <li class="dropdown">
-          <a class="dropdown-toggle waves-effect waves-light" data-toggle="dropdown">
-            <img src="{{ user|picture }}" class="picture img-circle" width="25" /> <span class="user-name">{{ user.name }}</span>
-          </a>
-          <ul class="dropdown-menu dropdown-menu-right">
-            <li><a href="/user/{{ user.username }}"><i class="icon-fw icon-home"></i>{{ t('Home') }}</a></li>
-            <li><a href="/me"><i class="icon-fw icon-wrench"></i>{{ t('User Settings') }}</a></li>
-            <li role="separator" class="divider"></li>
-            <li><a href="/user/{{ user.username }}#user-draft-list"><i class="icon-fw icon-docs"></i>{{ t('List Drafts') }}</a></li>
-            <li><a href="/trash"><i class="icon-fw icon-trash"></i>{{ t('Deleted Pages') }}</a></li>
-            <li role="separator" class="divider"></li>
-            <li><a href="/logout"><i class="icon-fw icon-power"></i>{{ t('Sign out') }}</a></li>
-          </ul>
-          <!-- /.dropdown-messages -->
-        </li>
+        <li id="personal-dropdown" class="dropdown"></li>
         {% else %}
         <li id="login-user"><a href="/login">Login</a></li>
         {% endif %}

Разница между файлами не показана из-за своего большого размера
+ 436 - 228
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов