jam411 3 lat temu
rodzic
commit
7012c42a08

+ 1 - 1
packages/app/_obsolete/src/client/app.jsx

@@ -30,7 +30,7 @@ import { Page } from '../components/Page';
 import DisplaySwitcher from '../components/Page/DisplaySwitcher';
 import RedirectedAlert from '../components/Page/RedirectedAlert';
 import ShareLinkAlert from '../components/Page/ShareLinkAlert';
-import PageComment from '../components/PageComment';
+import { PageComment } from '../components/PageComment';
 import CommentEditorLazyRenderer from '../components/PageComment/CommentEditorLazyRenderer';
 import PageContentFooter from '../components/PageContentFooter';
 import BookmarkList from '../components/PageList/BookmarkList';

+ 0 - 1
packages/app/package.json

@@ -212,7 +212,6 @@
     "handsontable": "=6.2.2",
     "i18next-hmr": "^1.7.7",
     "jquery-slimscroll": "^1.3.8",
-    "jquery-ui": "^1.12.1",
     "jquery.cookie": "~1.4.1",
     "jshint": "^2.13.0",
     "load-css-file": "^1.0.0",

+ 0 - 7
packages/app/resource/cdn-manifests.js

@@ -147,13 +147,6 @@ module.exports = {
         integrity: '',
       },
     },
-    {
-      name: 'jquery-ui',
-      url: 'https://cdn.jsdelivr.net/jquery.ui/1.11.4/jquery-ui.min.css',
-      args: {
-        integrity: '',
-      },
-    },
     {
       name: 'highlight-theme-github',
       url: 'https://cdn.jsdelivr.net/npm/highlight.js@9.13.0/styles/github.css',

+ 0 - 51
packages/app/src/client/services/AdminCustomizeContainer.js

@@ -19,8 +19,6 @@ export default class AdminCustomizeContainer extends Container {
 
     this.state = {
       retrieveError: null,
-      // set dummy value tile for using suspense
-      currentTheme: 'default',
       isEnabledTimeline: false,
       isSavedStatesOfTabChanges: false,
       isEnabledAttachTitleHeader: false,
@@ -77,7 +75,6 @@ export default class AdminCustomizeContainer extends Container {
       const { customizeParams } = response.data;
 
       this.setState({
-        currentTheme: customizeParams.themeType,
         isEnabledTimeline: customizeParams.isEnabledTimeline,
         isSavedStatesOfTabChanges: customizeParams.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: customizeParams.isEnabledAttachTitleHeader,
@@ -106,17 +103,6 @@ export default class AdminCustomizeContainer extends Container {
     }
   }
 
-  /**
-   * Switch themeType
-   */
-  switchThemeType(themeName) {
-    this.setState({ currentTheme: themeName });
-
-    // preview if production
-    if (process.env.NODE_ENV !== 'development') {
-      this.previewTheme(themeName);
-    }
-  }
 
   /**
    * Switch enabledTimeLine
@@ -236,24 +222,6 @@ export default class AdminCustomizeContainer extends Container {
     this.setState({ currentCustomizeScript: inpuValue });
   }
 
-  /**
-   * Preview theme
-   * @param {string} themeName
-   */
-  async previewTheme(themeName) {
-    try {
-      // get theme asset path
-      const response = await apiv3Get('/customize-setting/theme/asset-path', { themeName });
-      const { assetPath } = response.data;
-
-      const themeLink = document.getElementById('grw-theme-link');
-      themeLink.setAttribute('href', assetPath);
-    }
-    catch (err) {
-      toastError(err);
-    }
-  }
-
   /**
    * Preview hljs style
    * @param {string} styleId
@@ -265,25 +233,6 @@ export default class AdminCustomizeContainer extends Container {
     styleLInk.href = styleLInk.href.replace(/[^/]+\.css$/, `${styleId}.css`);
   }
 
-  /**
-   * Update theme
-   * @memberOf AdminCustomizeContainer
-   */
-  async updateCustomizeTheme() {
-    try {
-      const response = await apiv3Put('/customize-setting/theme', {
-        themeType: this.state.currentTheme,
-      });
-      const { customizedParams } = response.data;
-      this.setState({
-        themeType: customizedParams.themeType,
-      });
-    }
-    catch (err) {
-      logger.error(err);
-      throw new Error('Failed to update data');
-    }
-  }
 
   /**
    * Update function

+ 7 - 7
packages/app/src/components/Admin/Customize/Customize.jsx

@@ -1,5 +1,5 @@
 
-import React, { useEffect } from 'react';
+import React, { useEffect, useCallback } from 'react';
 
 import PropTypes from 'prop-types';
 
@@ -26,13 +26,9 @@ const logger = loggerFactory('growi:services:AdminCustomizePage');
 function Customize(props) {
   const { adminCustomizeContainer } = props;
 
-  useEffect(() => {
-    async function fetchCustomizeSettingsData() {
-      await adminCustomizeContainer.retrieveCustomizeData();
-    }
-
+  const fetchCustomizeSettingsData = useCallback(async() => {
     try {
-      fetchCustomizeSettingsData();
+      await adminCustomizeContainer.retrieveCustomizeData();
     }
     catch (err) {
       const errs = toArrayIfNot(err);
@@ -41,6 +37,10 @@ function Customize(props) {
     }
   }, [adminCustomizeContainer]);
 
+  useEffect(() => {
+    fetchCustomizeSettingsData();
+  }, [fetchCustomizeSettingsData]);
+
 
   return (
     <div data-testid="admin-customize">

+ 8 - 6
packages/app/src/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -6,7 +6,6 @@ import PropTypes from 'prop-types';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { GrowiThemes } from '~/interfaces/theme';
-import { useGrowiTheme } from '~/stores/context';
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 
@@ -50,10 +49,11 @@ const uniqueTheme = [{
 
 const CustomizeThemeOptions = (props) => {
 
-  const { adminCustomizeContainer } = props;
+  const { adminCustomizeContainer, currentTheme } = props;
+  const { currentLayout } = adminCustomizeContainer.state;
+
   const { t } = useTranslation();
-  const { mutate: mutateGrowiTheme } = useGrowiTheme();
-  const { currentLayout, currentTheme } = adminCustomizeContainer.state;
+
 
   return (
     <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
@@ -66,7 +66,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => mutateGrowiTheme(theme.name)}
+                onSelected={() => props.onSelected(theme.name)}
                 {...theme}
               />
             );
@@ -82,7 +82,7 @@ const CustomizeThemeOptions = (props) => {
               <ThemeColorBox
                 key={theme.name}
                 isSelected={currentTheme === theme.name}
-                onSelected={() => mutateGrowiTheme(theme.name)}
+                onSelected={() => props.onSelected(theme.name)}
                 {...theme}
               />
             );
@@ -98,6 +98,8 @@ const CustomizeThemeOptionsWrapper = withUnstatedContainers(CustomizeThemeOption
 
 CustomizeThemeOptions.propTypes = {
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
+  onSelected: PropTypes.func,
+  currentTheme: PropTypes.string,
 };
 
 export default CustomizeThemeOptionsWrapper;

+ 21 - 10
packages/app/src/components/Admin/Customize/CustomizeThemeSetting.tsx

@@ -4,6 +4,9 @@ import { useTranslation } from 'next-i18next';
 
 import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
+import { apiv3Put } from '~/client/util/apiv3-client';
+import { useGrowiTheme } from '~/stores/context';
+
 
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
@@ -17,28 +20,36 @@ type Props = {
 const CustomizeThemeSetting = (props: Props): JSX.Element => {
 
   const { adminCustomizeContainer } = props;
+  const { data: currentTheme, mutate: mutateGrowiTheme } = useGrowiTheme();
   const { t } = useTranslation();
 
+  const selectedHandler = useCallback((themeName) => {
+    mutateGrowiTheme(themeName);
+  }, [mutateGrowiTheme]);
+
   const submitHandler = useCallback(async() => {
     try {
-      await adminCustomizeContainer.updateCustomizeTheme();
+      if (currentTheme != null) {
+        await apiv3Put('/customize-setting/theme', {
+          themeType: currentTheme,
+        });
+      }
+
       toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
     }
     catch (err) {
       toastError(err);
     }
-  }, [t, adminCustomizeContainer]);
+  }, [currentTheme, t]);
 
   return (
-    <React.Fragment>
-      <div className="row">
-        <div className="col-12">
-          <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
-          <CustomizeThemeOptions />
-          <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
-        </div>
+    <div className="row">
+      <div className="col-12">
+        <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
+        <CustomizeThemeOptions onSelected={selectedHandler} currentTheme={currentTheme} />
+        <AdminUpdateButtonRow onClick={submitHandler} disabled={adminCustomizeContainer.state.retrieveError != null} />
       </div>
-    </React.Fragment>
+    </div>
   );
 };
 

+ 5 - 5
packages/app/src/components/Layout/AdminLayout.tsx

@@ -3,6 +3,8 @@ import React, { ReactNode } from 'react';
 import dynamic from 'next/dynamic';
 import { Provider } from 'unstated';
 
+import { AdminInjectableContainers } from '~/interfaces/unstated-container';
+
 import { GrowiNavbar } from '../Navbar/GrowiNavbar';
 
 import { RawLayout } from './RawLayout';
@@ -10,8 +12,6 @@ import { RawLayout } from './RawLayout';
 import styles from './Admin.module.scss';
 
 
-// import { injectableContainers } from '~/client/admin';
-
 type Props = {
   title: string
   /**
@@ -21,11 +21,12 @@ type Props = {
    */
   selectedNavOpt: string
   children?: ReactNode
+  injectableContainers: AdminInjectableContainers
 }
 
 
 const AdminLayout = ({
-  children, title, selectedNavOpt,
+  children, title, selectedNavOpt, injectableContainers,
 }: Props): JSX.Element => {
 
   const AdminNavigation = dynamic(() => import('~/components/Admin/Common/AdminNavigation'), { ssr: false });
@@ -45,8 +46,7 @@ const AdminLayout = ({
               <AdminNavigation selected={selectedNavOpt} />
             </div>
             <div className="col-lg-9">
-              {/* TODO: inject Admincontainer (injectableContainers & adminSecurityContainers) by https://redmine.weseek.co.jp/issues/100072 */}
-              <Provider>
+              <Provider inject={injectableContainers}>
                 {children}
               </Provider>
             </div>

+ 20 - 13
packages/app/src/components/PageComment.tsx

@@ -2,41 +2,41 @@ import React, {
   FC, useEffect, useState, useMemo, memo, useCallback,
 } from 'react';
 
+import { Nullable } from '@growi/core';
 import { Button } from 'reactstrap';
 
-
-import AppContainer from '~/client/services/AppContainer';
 import { toastError } from '~/client/util/apiNotification';
 import { apiPost } from '~/client/util/apiv1-client';
+import { useCurrentPagePath } from '~/stores/context';
+import { useSWRxCurrentPage } from '~/stores/page';
 import { useCommentPreviewOptions } from '~/stores/renderer';
 
 import { ICommentHasId, ICommentHasIdList } from '../interfaces/comment';
 import { useSWRxPageComment } from '../stores/comment';
 
-
-import Comment from './PageComment/Comment';
+import { Comment } from './PageComment/Comment';
 import { CommentEditor } from './PageComment/CommentEditor';
 import DeleteCommentModal from './PageComment/DeleteCommentModal';
 import { ReplayComments } from './PageComment/ReplayComments';
 
 type Props = {
-  appContainer: AppContainer,
-  pageId: string,
+  pageId?: Nullable<string>, // TODO: check pageId type
   isReadOnly : boolean,
   titleAlign?: 'center' | 'left' | 'right',
   highlightKeywords?:string[],
   hideIfEmpty?: boolean,
 }
 
-
-const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
+export const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
 
   const {
-    appContainer, pageId, highlightKeywords, isReadOnly, titleAlign, hideIfEmpty,
+    pageId, highlightKeywords, isReadOnly, titleAlign, hideIfEmpty,
   } = props;
 
   const { data: comments, mutate } = useSWRxPageComment(pageId);
   const { data: rendererOptions } = useCommentPreviewOptions();
+  const { data: currentPage } = useSWRxCurrentPage();
+  const { data: currentPagePath } = useCurrentPagePath();
 
   const [commentToBeDeleted, setCommentToBeDeleted] = useState<ICommentHasId | null>(null);
   const [isDeleteConfirmModalShown, setIsDeleteConfirmModalShown] = useState<boolean>(false);
@@ -110,6 +110,16 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
     }
   }, [commentToBeDeleted, onDeleteCommentAfterOperation]);
 
+  const generateAllRepliesElement = (replyComments: ICommentHasIdList) => (
+    // TODO: need page props path
+    <ReplayComments
+      replyList={replyComments}
+      deleteBtnClicked={onClickDeleteButton}
+      rendererOptions={rendererOptions}
+      isReadOnly={isReadOnly}
+    />
+  );
+
   const removeShowEditorId = useCallback((commentId: string) => {
     setShowEditorIds((previousState) => {
       const previousShowEditorIds = new Set(...previousState);
@@ -124,8 +134,7 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
   if (hideIfEmpty && comments?.length === 0) {
     return <></>;
   }
-
-  if (rendererOptions == null) {
+  if (rendererOptions == null || currentPagePath == null) {
     return <></>;
   }
 
@@ -223,5 +232,3 @@ const PageComment:FC<Props> = memo((props:Props):JSX.Element => {
 });
 
 PageComment.displayName = 'PageComment';
-
-export default PageComment;

+ 178 - 0
packages/app/src/components/PageComment/Comment.tsx

@@ -0,0 +1,178 @@
+import React, { useEffect, useState } from 'react';
+
+
+import { UserPicture } from '@growi/ui';
+import { ConsoleFormattedStream } from 'browser-bunyan';
+import { format } from 'date-fns';
+import { useTranslation } from 'next-i18next';
+import { UncontrolledTooltip } from 'reactstrap';
+
+import { RendererOptions } from '~/services/renderer/renderer';
+import { useCurrentUser } from '~/stores/context';
+
+import { ICommentHasId } from '../../interfaces/comment';
+import FormattedDistanceDate from '../FormattedDistanceDate';
+import HistoryIcon from '../Icons/HistoryIcon';
+import RevisionRenderer from '../Page/RevisionRenderer';
+import Username from '../User/Username';
+
+import CommentControl from './CommentControl';
+import CommentEditor from './CommentEditor';
+
+type CommentProps = {
+  comment: ICommentHasId,
+  isReadOnly: boolean,
+  deleteBtnClicked: (comment: ICommentHasId) => void,
+  onComment: () => void,
+  rendererOptions: RendererOptions,
+  currentPagePath: string,
+  currentRevisionId: string,
+  currentRevisionCreatedAt: Date,
+}
+
+export const Comment = (props: CommentProps): JSX.Element => {
+  const {
+    comment, isReadOnly, deleteBtnClicked, onComment, rendererOptions, currentPagePath, currentRevisionId, currentRevisionCreatedAt,
+  } = props;
+  const { t } = useTranslation();
+  const { data: currentUser } = useCurrentUser();
+
+  const [markdown, setMarkdown] = useState('');
+  const [isReEdit, setIsReEdit] = useState(false);
+
+  const commentId = comment._id;
+  const creator = comment.creator;
+  const isMarkdown = comment.isMarkdown;
+  const createdAt = new Date(comment.createdAt);
+  const updatedAt = new Date(comment.updatedAt);
+  const isEdited = createdAt < updatedAt;
+
+  useEffect(() => {
+    setMarkdown(comment.comment);
+
+    const isCurrentRevision = () => {
+      return comment.revision === currentRevisionId;
+    };
+    isCurrentRevision();
+
+  }, [comment, currentRevisionId]);
+
+  const isCurrentUserEqualsToAuthor = () => {
+    const { creator }: any = comment;
+
+    if (creator == null || currentUser == null) {
+      return false;
+    }
+    return creator.username === currentUser.username;
+  };
+
+  const getRootClassName = (comment) => {
+    let className = 'page-comment flex-column';
+
+    if (comment.revision === currentRevisionId) {
+      className += ' page-comment-current';
+    }
+    else if (comment.createdAt.getTime() > currentRevisionCreatedAt.getTime()) {
+      className += ' page-comment-newer';
+    }
+    else {
+      className += ' page-comment-older';
+    }
+
+    if (isCurrentUserEqualsToAuthor()) {
+      className += ' page-comment-me';
+    }
+
+    return className;
+  };
+
+  const deleteBtnClickedHandler = (comment) => {
+    deleteBtnClicked(comment);
+  };
+
+  const renderText = (comment) => {
+    return <span style={{ whiteSpace: 'pre-wrap' }}>{comment}</span>;
+  };
+
+  // TODO: Remove when update ReplayComments.jsx
+  if (currentPagePath == null) {
+    return <></>;
+  }
+
+  const renderRevisionBody = () => {
+    return (
+      <RevisionRenderer
+        rendererOptions={rendererOptions}
+        markdown={markdown}
+        additionalClassName="comment"
+        pagePath={currentPagePath}
+      />
+    );
+  };
+
+  const rootClassName = getRootClassName(comment);
+  const commentBody = isMarkdown ? renderRevisionBody() : renderText(comment.comment);
+  const revHref = `?revision=${comment.revision}`;
+
+  const editedDateId = `editedDate-${comment._id}`;
+  const editedDateFormatted = isEdited
+    ? format(updatedAt, 'yyyy/MM/dd HH:mm')
+    : null;
+
+  return (
+    <>
+      {(isReEdit && !isReadOnly) ? (
+        <CommentEditor
+          rendererOptions={rendererOptions}
+          currentCommentId={commentId}
+          commentBody={comment.comment}
+          replyTo={undefined}
+          commentCreator={creator?.username}
+          onCancelButtonClicked={() => setIsReEdit(false)}
+          onCommentButtonClicked={() => {
+            setIsReEdit(false);
+            if (onComment != null) onComment();
+          }}
+        />
+      ) : (
+        <div id={commentId} className={rootClassName}>
+          <div className="page-comment-writer">
+            <UserPicture user={creator} />
+          </div>
+          <div className="page-comment-main">
+            <div className="page-comment-creator">
+              <Username user={creator} />
+            </div>
+            <div className="page-comment-body">{commentBody}</div>
+            <div className="page-comment-meta">
+              <a href={`#${commentId}`}>
+                <FormattedDistanceDate id={commentId} date={comment.createdAt} />
+              </a>
+              { isEdited && (
+                <>
+                  <span id={editedDateId}>&nbsp;(edited)</span>
+                  <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
+                </>
+              )}
+              <span className="ml-2">
+                <a id={`page-comment-revision-${commentId}`} className="page-comment-revision" href={revHref}>
+                  <HistoryIcon />
+                </a>
+                <UncontrolledTooltip placement="bottom" fade={false} target={`page-comment-revision-${commentId}`}>
+                  {t('page_comment.display_the_page_when_posting_this_comment')}
+                </UncontrolledTooltip>
+              </span>
+            </div>
+            {(isCurrentUserEqualsToAuthor() && !isReadOnly) && (
+              <CommentControl
+                onClickDeleteBtn={deleteBtnClickedHandler}
+                onClickEditBtn={() => setIsReEdit(true)}
+              />
+            ) }
+          </div>
+        </div>
+      )
+      }
+    </>
+  );
+};

+ 6 - 6
packages/app/src/components/PageComment/CommentEditor.tsx

@@ -268,18 +268,18 @@ export const CommentEditor = (props: PropsType): JSX.Element => {
           <CustomNavTab activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={handleSelect} hideBorderBottom />
           <TabContent activeTab={activeTab}>
             <TabPane tabId="comment_editor">
-              <AnyEditor
+              {/* <AnyEditor
                 ref={editorRef}
                 value={comment}
                 lineNumbers={false}
                 isMobile={isMobile}
-                isUploadable={isUploadable}
-                isUploadableFile={isUploadableFile}
+                // isUploadable={isUploadable}
+                // isUploadableFile={isUploadableFile}
                 onChange={setComment}
                 onUpload={uploadHandler}
                 onCtrlEnter={ctrlEnterHandler}
                 isComment
-              />
+              /> */}
               {/*
                 Note: <OptionsSelector /> is not optimized for ComentEditor in terms of responsive design.
                 See a review comment in https://github.com/weseek/growi/pull/3473
@@ -298,7 +298,7 @@ export const CommentEditor = (props: PropsType): JSX.Element => {
             <span className="flex-grow-1" />
             <span className="d-none d-sm-inline">{ errorMessage && errorMessage }</span>
 
-            { isSlackConfigured
+            {/* { isSlackConfigured
               && (
                 <div className="form-inline align-self-center mr-md-2">
                   <SlackNotification
@@ -310,7 +310,7 @@ export const CommentEditor = (props: PropsType): JSX.Element => {
                   />
                 </div>
               )
-            }
+            } */}
             <div className="d-none d-sm-block">
               <span className="mr-2">{cancelButton}</span><span>{submitButton}</span>
             </div>

+ 8 - 0
packages/app/src/components/PageComment/CommentEditorLazyRenderer.tsx

@@ -1,5 +1,7 @@
 import React, { FC } from 'react';
 
+import { useCommentPreviewOptions } from '~/stores/renderer';
+
 import { useSWRxPageComment } from '../../stores/comment';
 
 import { CommentEditor } from './CommentEditor';
@@ -12,9 +14,15 @@ const CommentEditorLazyRenderer:FC<Props> = (props:Props):JSX.Element => {
 
   const { pageId } = props;
   const { mutate } = useSWRxPageComment(pageId);
+  const { data: rendererOptions } = useCommentPreviewOptions();
+
+  if (rendererOptions == null) {
+    return <></>;
+  }
 
   return (
     <CommentEditor
+      rendererOptions={rendererOptions}
       replyTo={undefined}
       onCommentButtonClicked={mutate}
       isForNewComment

+ 114 - 0
packages/app/src/components/PageComment/ReplayComments.jsx

@@ -0,0 +1,114 @@
+import React from 'react';
+
+import PropTypes from 'prop-types';
+import { Collapse } from 'reactstrap';
+
+import { RendererOptions } from '~/services/renderer/renderer';
+import { useRendererConfig } from '~/stores/context';
+
+import { Comment } from './Comment';
+
+
+class ReplayComments extends React.PureComponent {
+
+  constructor() {
+    super();
+
+    this.state = {
+      isOlderRepliesShown: false,
+    };
+
+    this.toggleOlderReplies = this.toggleOlderReplies.bind(this);
+  }
+
+  toggleOlderReplies() {
+    this.setState({ isOlderRepliesShown: !this.state.isOlderRepliesShown });
+  }
+
+  renderReply(reply) {
+    return (
+      <div key={reply._id} className="page-comment-reply ml-4 ml-sm-5 mr-3">
+        <Comment
+          comment={reply}
+          deleteBtnClicked={this.props.deleteBtnClicked}
+          rendererOptions={this.props.rendererOptions}
+          isReadOnly={this.props.isReadOnly}
+        />
+      </div>
+    );
+  }
+
+  render() {
+    const { config } = this.props;
+
+    const isAllReplyShown = config.isAllReplyShown || false;
+    const replyList = this.props.replyList;
+
+    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 isOpen={this.state.isOlderRepliesShown}>
+              <div>{hiddenElements}</div>
+            </Collapse>
+            <div className="text-center">
+              <button
+                type="button"
+                className="btn btn-link"
+                onClick={this.toggleOlderReplies}
+              >
+                {toggleButtonIcon} {toggleButtonLabel}
+              </button>
+            </div>
+          </div>
+        )}
+        {shownElements}
+
+      </React.Fragment>
+    );
+  }
+
+}
+
+ReplayComments.propTypes = {
+  rendererOptions: PropTypes.instanceOf(RendererOptions).isRequired,
+  deleteBtnClicked: PropTypes.func.isRequired,
+  isReadOnly: PropTypes.bool.isRequired,
+  replyList: PropTypes.array,
+};
+
+const ReplayCommentsWrapperFC = (props) => {
+  const { data: config } = useRendererConfig();
+
+  return <ReplayComments config={config} {...props} />;
+};
+
+export default ReplayCommentsWrapperFC;

+ 3 - 9
packages/app/src/components/PageEditor/DrawioModal.jsx

@@ -1,15 +1,12 @@
 import React from 'react';
-import PropTypes from 'prop-types';
-import i18next from 'i18next';
 
+import i18next from 'i18next';
+import PropTypes from 'prop-types';
 import {
   Modal,
   ModalBody,
 } from 'reactstrap';
 
-import { withUnstatedContainers } from '../UnstatedUtils';
-import AppContainer from '~/client/services/AppContainer';
-import EditorContainer from '~/client/services/EditorContainer';
 import { getDiagramsNetLangCode } from '~/client/util/locale-utils';
 
 class DrawioModal extends React.PureComponent {
@@ -168,11 +165,8 @@ class DrawioModal extends React.PureComponent {
 }
 
 DrawioModal.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
-
   onSave: PropTypes.func,
 };
 
 
-export default withUnstatedContainers(DrawioModal, [AppContainer, EditorContainer]);
+export default DrawioModal;

+ 5 - 15
packages/app/src/components/PageEditor/Preview.tsx

@@ -2,14 +2,11 @@ import React, {
   useCallback, useEffect, useMemo, useState, SyntheticEvent, RefObject,
 } from 'react';
 
-
-import AppContainer from '~/client/services/AppContainer';
 import InterceptorManager from '~/services/interceptor-manager';
 import { RendererOptions } from '~/services/renderer/renderer';
 import { useEditorSettings } from '~/stores/editor';
 
 import RevisionBody from '../Page/RevisionBody';
-import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 declare const interceptorManager: InterceptorManager;
@@ -23,9 +20,7 @@ type Props = {
   onScroll?: (scrollTop: number) => void,
 }
 
-type UnstatedProps = Props & { appContainer: AppContainer };
-
-const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivElement>): JSX.Element => {
+const Preview = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>): JSX.Element => {
 
   const {
     rendererOptions,
@@ -108,16 +103,11 @@ const Preview = React.forwardRef((props: UnstatedProps, ref: RefObject<HTMLDivEl
 
 Preview.displayName = 'Preview';
 
-/**
- * Wrapper component for using unstated
- */
-const PreviewWrapper = withUnstatedContainers(Preview, [AppContainer]);
-
 // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types
-const PreviewWrapper2 = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>): JSX.Element => {
-  return <PreviewWrapper ref={ref} {...props} />;
+const PreviewWrapper = React.forwardRef((props: Props, ref: RefObject<HTMLDivElement>): JSX.Element => {
+  return <Preview ref={ref} {...props} />;
 });
 
-PreviewWrapper2.displayName = 'PreviewWrapper2';
+PreviewWrapper.displayName = 'PreviewWrapper';
 
-export default PreviewWrapper2;
+export default PreviewWrapper;

+ 2 - 2
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -24,7 +24,7 @@ import { AdditionalMenuItemsRendererProps, ForceHideMenuItems } from '../Common/
 import { GrowiSubNavigation } from '../Navbar/GrowiSubNavigation';
 import { SubNavButtons } from '../Navbar/SubNavButtons';
 import RevisionLoader from '../Page/RevisionLoader';
-import PageComment from '../PageComment';
+import { PageComment } from '../PageComment';
 import { PageContentFooter } from '../PageContentFooter';
 
 
@@ -214,7 +214,7 @@ export const SearchResultContent: FC<Props> = (props: Props) => {
           revisionId={page.revision}
           highlightKeywords={highlightKeywords}
         />
-        <PageComment appContainer={appContainer} pageId={page._id} highlightKeywords={highlightKeywords} isReadOnly hideIfEmpty />
+        <PageComment pageId={page._id} highlightKeywords={highlightKeywords} isReadOnly hideIfEmpty />
         <PageContentFooter
           // createdAt={new Date(pageWithMeta.data.createdAt)}
           // updatedAt={new Date(pageWithMeta.data.updatedAt)}

+ 28 - 0
packages/app/src/interfaces/unstated-container.ts

@@ -0,0 +1,28 @@
+import AdminAppContainer from '~/client/services/AdminAppContainer';
+import AdminBasicSecurityContainer from '~/client/services/AdminBasicSecurityContainer';
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
+import AdminExternalAccountsContainer from '~/client/services/AdminExternalAccountsContainer';
+import AdminGeneralSecurityContainer from '~/client/services/AdminGeneralSecurityContainer';
+import AdminGitHubSecurityContainer from '~/client/services/AdminGitHubSecurityContainer';
+import AdminGoogleSecurityContainer from '~/client/services/AdminGoogleSecurityContainer';
+import AdminHomeContainer from '~/client/services/AdminHomeContainer';
+import AdminImportContainer from '~/client/services/AdminImportContainer';
+import AdminLdapSecurityContainer from '~/client/services/AdminLdapSecurityContainer';
+import AdminLocalSecurityContainer from '~/client/services/AdminLocalSecurityContainer';
+import AdminMarkDownContainer from '~/client/services/AdminMarkDownContainer';
+import AdminNotificationContainer from '~/client/services/AdminNotificationContainer';
+import AdminOidcSecurityContainer from '~/client/services/AdminOidcSecurityContainer';
+import AdminSamlSecurityContainer from '~/client/services/AdminSamlSecurityContainer';
+import AdminSlackIntegrationLegacyContainer from '~/client/services/AdminSlackIntegrationLegacyContainer';
+import AdminTwitterSecurityContainer from '~/client/services/AdminTwitterSecurityContainer';
+import AdminUserGroupDetailContainer from '~/client/services/AdminUserGroupDetailContainer';
+import AdminUsersContainer from '~/client/services/AdminUsersContainer';
+
+type AdminUnstatedContainers =
+  AdminAppContainer | AdminBasicSecurityContainer | AdminCustomizeContainer | AdminExternalAccountsContainer |
+  AdminGeneralSecurityContainer | AdminGitHubSecurityContainer | AdminGoogleSecurityContainer | AdminHomeContainer |
+  AdminImportContainer | AdminLdapSecurityContainer | AdminLocalSecurityContainer | AdminMarkDownContainer |
+  AdminNotificationContainer | AdminOidcSecurityContainer | AdminSamlSecurityContainer | AdminSlackIntegrationLegacyContainer |
+  AdminTwitterSecurityContainer | AdminUserGroupDetailContainer | AdminUsersContainer;
+
+export type AdminInjectableContainers = AdminUnstatedContainers[];

+ 4 - 2
packages/app/src/pages/[[...path]].page.tsx

@@ -18,8 +18,9 @@ import { useRouter } from 'next/router';
 import superjson from 'superjson';
 
 import { PageAlerts } from '~/components/PageAlert/PageAlerts';
-// import { PageComments } from '~/components/PageComment/PageComments';
+import { PageComment } from '~/components/PageComment';
 // import { useTranslation } from '~/i18n';
+import CommentEditorLazyRenderer from '~/components/PageComment/CommentEditorLazyRenderer';
 import { PageContentFooter } from '~/components/PageContentFooter';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 // import { renderScriptTagByName, renderHighlightJsStyleTag } from '~/service/cdn-resources-loader';
@@ -327,7 +328,8 @@ const GrowiPage: NextPage<Props> = (props: Props) => {
         </div>
         <footer>
           {/* <PageComments /> */}
-          PageComments
+          <PageComment pageId={useCurrentPageId().data} isReadOnly={false} titleAlign="left" />
+          {/* <CommentEditorLazyRenderer pageId={useCurrentPageId().data} /> */}
           <PageContentFooter />
         </footer>
 

+ 20 - 1
packages/app/src/pages/admin/[[...path]].page.tsx

@@ -8,6 +8,7 @@ import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
 import dynamic from 'next/dynamic';
 import { useRouter } from 'next/router';
 
+import AdminCustomizeContainer from '~/client/services/AdminCustomizeContainer';
 import { CrowiRequest } from '~/interfaces/crowi-request';
 import PluginUtils from '~/server/plugins/plugin-utils';
 import ConfigLoader from '~/server/service/config-loader';
@@ -18,6 +19,8 @@ import {
 import {
   CommonProps, getServerSideCommonProps, useCustomTitle, getNextI18NextConfig,
 } from '../utils/commons';
+
+
 // import { useEnvVars } from '~/stores/admin-context';
 
 const AdminHome = dynamic(() => import('../../components/Admin/AdminHome/AdminHome'), { ssr: false });
@@ -145,8 +148,24 @@ const AdminMarkdownSettingsPage: NextPage<Props> = (props: Props) => {
 
   // useEnvVars(props.envVars);
 
+
+  const adminCustomizeContainer = new AdminCustomizeContainer();
+
+  const injectableContainers = [
+    // adminAppContainer,
+    // adminImportContainer,
+    // adminHomeContainer,
+    adminCustomizeContainer,
+    // adminUsersContainer,
+    // adminExternalAccountsContainer,
+    // adminNotificationContainer,
+    // adminSlackIntegrationLegacyContainer,
+    // adminMarkDownContainer,
+    // adminUserGroupDetailContainer,
+  ];
+
   return (
-    <AdminLayout title={title} selectedNavOpt={name}>
+    <AdminLayout title={title} selectedNavOpt={name} injectableContainers={injectableContainers}>
       {content.component}
     </AdminLayout>
   );

+ 1 - 11
packages/app/src/server/views/admin/customize.html

@@ -1,16 +1,6 @@
 {% extends '../layout/admin.html' %}
-{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Customize')) }}{% endblock %}
 
-{% block html_additional_headers %}
-{% parent %}
-<!-- CodeMirror -->
-{{ cdnStyleTag('jquery-ui') }}
-<style>
-  .CodeMirror {
-    border: 1px solid $gray-100;
-  }
-</style>
-{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Customize')) }}{% endblock %}
 
 {% block content_header %}
 <h1 class="title">{{ t('Customize') }}</h1>

+ 9 - 0
packages/app/src/services/renderer/renderer.tsx

@@ -340,6 +340,15 @@ export const generatePreviewOptions: ReactMarkdownOptionsGenerator = (config: Re
 
 export const generateCommentPreviewOptions: ReactMarkdownOptionsGenerator = (config: RendererConfig): RendererOptions => {
   const options = generateCommonOptions(config);
+  const { remarkPlugins } = options;
+
+  // add remark plugins
+  if (remarkPlugins != null) {
+    remarkPlugins.push(emoji);
+    if (config.isEnabledLinebreaksInComments) {
+      remarkPlugins.push(breaks);
+    }
+  }
 
   // renderer.addConfigurers([
   //   new TableConfigurer(),

+ 1 - 8
yarn.lock

@@ -12614,18 +12614,11 @@ jquery-slimscroll@^1.3.8:
   dependencies:
     jquery ">= 1.7"
 
-jquery-ui@^1.12.1:
-  version "1.13.0"
-  resolved "https://registry.yarnpkg.com/jquery-ui/-/jquery-ui-1.13.0.tgz#ab5ac65f37ca093c51b3478c4097f55bbc008f36"
-  integrity sha512-Osf7ECXNTYHtKBkn9xzbIf9kifNrBhfywFEKxOeB/OVctVmLlouV9mfc2qXCp6uyO4Pn72PXKOnj09qXetopCw==
-  dependencies:
-    jquery ">=1.8.0 <4.0.0"
-
 jquery.cookie@~1.4.1:
   version "1.4.1"
   resolved "https://registry.yarnpkg.com/jquery.cookie/-/jquery.cookie-1.4.1.tgz#d63dce209eab691fe63316db08ca9e47e0f9385b"
 
-"jquery@>= 1.7", jquery@>=1.12.0, "jquery@>=1.8.0 <4.0.0":
+"jquery@>= 1.7", jquery@>=1.12.0:
   version "3.6.0"
   resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470"
   integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==