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

Merge branch 'master' into imprv/omit-insecure-attributes

Yuki Takei 5 лет назад
Родитель
Сommit
9dc90120de

+ 1 - 1
src/client/js/app.jsx

@@ -89,7 +89,7 @@ Object.assign(componentMappings, {
 
 
   'not-found-alert': <NotFoundAlert
   'not-found-alert': <NotFoundAlert
     onPageCreateClicked={navigationContainer.setEditorMode}
     onPageCreateClicked={navigationContainer.setEditorMode}
-    isGuestUserMode={appContainer.currentUser == null}
+    isGuestUserMode={appContainer.isGuestUser}
     isHidden={pageContainer.state.isForbidden || pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage}
     isHidden={pageContainer.state.isForbidden || pageContainer.state.isNotCreatable || pageContainer.state.isTrashPage}
   />,
   />,
 
 

+ 10 - 7
src/client/js/components/Admin/UserGroup/UserGroupPage.jsx

@@ -152,13 +152,16 @@ class UserGroupPage extends React.Component {
           onDelete={this.showDeleteModal}
           onDelete={this.showDeleteModal}
           userGroupRelations={this.state.userGroupRelations}
           userGroupRelations={this.state.userGroupRelations}
         />
         />
-        <PaginationWrapper
-          activePage={this.state.activePage}
-          changePage={this.handlePage}
-          totalItemsCount={this.state.totalUserGroups}
-          pagingLimit={this.state.pagingLimit}
-          size="sm"
-        />
+        {this.state.userGroups.length === 0
+        ? <p>No groups yet</p> : (
+          <PaginationWrapper
+            activePage={this.state.activePage}
+            changePage={this.handlePage}
+            totalItemsCount={this.state.totalUserGroups}
+            pagingLimit={this.state.pagingLimit}
+            size="sm"
+          />
+        )}
         <UserGroupDeleteModal
         <UserGroupDeleteModal
           userGroups={this.state.userGroups}
           userGroups={this.state.userGroups}
           deleteUserGroup={this.state.selectedUserGroup}
           deleteUserGroup={this.state.selectedUserGroup}

+ 9 - 8
src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -58,14 +58,15 @@ class UserGroupPageList extends React.Component {
         <ul className="page-list-ul page-list-ul-flat mb-3">
         <ul className="page-list-ul page-list-ul-flat mb-3">
           {this.state.currentPages.map(page => <li key={page._id}><Page page={page} /></li>)}
           {this.state.currentPages.map(page => <li key={page._id}><Page page={page} /></li>)}
         </ul>
         </ul>
-        {adminUserGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : null}
-        <PaginationWrapper
-          activePage={this.state.activePage}
-          changePage={this.handlePageChange}
-          totalItemsCount={this.state.total}
-          pagingLimit={this.state.pagingLimit}
-          size="sm"
-        />
+        {adminUserGroupDetailContainer.state.relatedPages.length === 0 ? <p>{t('admin:user_group_management.no_pages')}</p> : (
+          <PaginationWrapper
+            activePage={this.state.activePage}
+            changePage={this.handlePageChange}
+            totalItemsCount={this.state.total}
+            pagingLimit={this.state.pagingLimit}
+            size="sm"
+          />
+        )}
       </Fragment>
       </Fragment>
     );
     );
   }
   }

+ 3 - 3
src/client/js/components/Admin/Users/UserInviteModal.jsx

@@ -131,9 +131,9 @@ class UserInviteModal extends React.Component {
         {userList.map((user) => {
         {userList.map((user) => {
           const copyText = `Email:${user.email} Password:${user.password} `;
           const copyText = `Email:${user.email} Password:${user.password} `;
           return (
           return (
-            <div className="my-1">
-              <CopyToClipboard key={user.email} text={copyText} onCopy={this.showToaster}>
-                <li key={user.email} className="btn btn-outline-secondary">
+            <div className="my-1" key={user.email}>
+              <CopyToClipboard text={copyText} onCopy={this.showToaster}>
+                <li className="btn btn-outline-secondary">
                 Email: <strong className="mr-3">{user.email}</strong> Password: <strong>{user.password}</strong>
                 Email: <strong className="mr-3">{user.email}</strong> Password: <strong>{user.password}</strong>
                 </li>
                 </li>
               </CopyToClipboard>
               </CopyToClipboard>

+ 11 - 1
src/client/js/components/Navbar/GrowiNavbar.jsx

@@ -3,10 +3,12 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { UncontrolledTooltip } from 'reactstrap';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import NavigationContainer from '../../services/NavigationContainer';
 import NavigationContainer from '../../services/NavigationContainer';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
 
 
+
 import GrowiLogo from '../Icons/GrowiLogo';
 import GrowiLogo from '../Icons/GrowiLogo';
 
 
 import PersonalDropdown from './PersonalDropdown';
 import PersonalDropdown from './PersonalDropdown';
@@ -45,10 +47,18 @@ class GrowiNavbar extends React.Component {
 
 
     return (
     return (
       <li className="nav-item confidential text-light">
       <li className="nav-item confidential text-light">
-        <i className="icon-info d-md-none" data-toggle="tooltip" title={crowi.confidential} />
+        <i id="confidentialTooltip" className="icon-info d-md-none" />
         <span className="d-none d-md-inline">
         <span className="d-none d-md-inline">
           {crowi.confidential}
           {crowi.confidential}
         </span>
         </span>
+        <UncontrolledTooltip
+          placement="bottom"
+          trigger="click"
+          target="confidentialTooltip"
+          className="d-md-none"
+        >
+          {crowi.confidential}
+        </UncontrolledTooltip>
       </li>
       </li>
     );
     );
   }
   }

+ 1 - 1
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -141,7 +141,7 @@ const GrowiSubNavigation = (props) => {
 
 
         <div className="d-flex flex-column align-items-end">
         <div className="d-flex flex-column align-items-end">
           <div className="d-flex">
           <div className="d-flex">
-            { !isPageInTrash && !isPageNotFound && !isPageForbidden && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
+            { pageContainer.isAbleToShowPageReactionButtons && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
             { !isPageNotFound && !isPageForbidden && <PageManagement isCompactMode={isCompactMode} /> }
             { !isPageNotFound && !isPageForbidden && <PageManagement isCompactMode={isCompactMode} /> }
           </div>
           </div>
           <div className="mt-2">
           <div className="mt-2">

+ 4 - 4
src/client/js/components/Page/NotFoundAlert.jsx

@@ -41,10 +41,10 @@ const NotFoundAlert = (props) => {
 
 
 
 
         {isGuestUserMode && (
         {isGuestUserMode && (
-        <UncontrolledTooltip placement="bottom" target="create-page-btn-wrapper-for-tooltip" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
+          <UncontrolledTooltip placement="bottom" target="create-page-btn-wrapper-for-tooltip" fade={false}>
+            {t('Not available for guest')}
+          </UncontrolledTooltip>
+        )}
       </div>
       </div>
     </div>
     </div>
   );
   );

+ 4 - 8
src/client/js/components/PageAccessories.jsx

@@ -10,18 +10,14 @@ import PageAccessoriesContainer from '../services/PageAccessoriesContainer';
 
 
 const PageAccessories = (props) => {
 const PageAccessories = (props) => {
   const { appContainer, pageAccessoriesContainer } = props;
   const { appContainer, pageAccessoriesContainer } = props;
-  const isGuestUserMode = appContainer.currentUser == null;
-
-  // not render only when this page is shared and user is not login.
-  if (appContainer.isSharedUser && isGuestUserMode) {
-    return null;
-  }
+  const { isGuestUser, isSharedUser } = appContainer;
 
 
   return (
   return (
     <>
     <>
-      <PageAccessoriesModalControl isGuestUserMode={isGuestUserMode} />
+      <PageAccessoriesModalControl isGuestUser={isGuestUser} isSharedUser={isSharedUser} />
       <PageAccessoriesModal
       <PageAccessoriesModal
-        isGuestUserMode={isGuestUserMode}
+        isGuestUser={isGuestUser}
+        isSharedUser={isSharedUser}
         isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
         isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
         onClose={pageAccessoriesContainer.closePageAccessoriesModal}
         onClose={pageAccessoriesContainer.closePageAccessoriesModal}
       />
       />

+ 14 - 10
src/client/js/components/PageAccessoriesModal.jsx

@@ -24,7 +24,7 @@ import ExpandOrContractButton from './ExpandOrContractButton';
 
 
 const PageAccessoriesModal = (props) => {
 const PageAccessoriesModal = (props) => {
   const {
   const {
-    t, pageAccessoriesContainer, onClose, isGuestUserMode,
+    t, pageAccessoriesContainer, onClose, isGuestUser, isSharedUser,
   } = props;
   } = props;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { activeTab, activeComponents } = pageAccessoriesContainer.state;
   const { activeTab, activeComponents } = pageAccessoriesContainer.state;
@@ -36,16 +36,19 @@ const PageAccessoriesModal = (props) => {
         Icon: PageListIcon,
         Icon: PageListIcon,
         i18n: t('page_list'),
         i18n: t('page_list'),
         index: 0,
         index: 0,
+        isLinkEnabled: v => !isSharedUser,
       },
       },
-      timeline:  {
+      timeline: {
         Icon: TimeLineIcon,
         Icon: TimeLineIcon,
         i18n: t('Timeline View'),
         i18n: t('Timeline View'),
         index: 1,
         index: 1,
+        isLinkEnabled: v => !isSharedUser,
       },
       },
       pageHistory: {
       pageHistory: {
         Icon: HistoryIcon,
         Icon: HistoryIcon,
         i18n: t('History'),
         i18n: t('History'),
         index: 2,
         index: 2,
+        isLinkEnabled: v => !isGuestUser && !isSharedUser,
       },
       },
       attachment: {
       attachment: {
         Icon: AttachmentIcon,
         Icon: AttachmentIcon,
@@ -56,10 +59,10 @@ const PageAccessoriesModal = (props) => {
         Icon: ShareLinkIcon,
         Icon: ShareLinkIcon,
         i18n: t('share_links.share_link_management'),
         i18n: t('share_links.share_link_management'),
         index: 4,
         index: 4,
-        isLinkEnabled: v => !isGuestUserMode,
+        isLinkEnabled: v => !isGuestUser && !isSharedUser,
       },
       },
     };
     };
-  }, [t, isGuestUserMode]);
+  }, [t, isGuestUser, isSharedUser]);
 
 
   const closeModalHandler = useCallback(() => {
   const closeModalHandler = useCallback(() => {
     if (onClose == null) {
     if (onClose == null) {
@@ -106,15 +109,15 @@ const PageAccessoriesModal = (props) => {
             <TabPane tabId="timeline">
             <TabPane tabId="timeline">
               {activeComponents.has('timeline') && <PageTimeline /> }
               {activeComponents.has('timeline') && <PageTimeline /> }
             </TabPane>
             </TabPane>
-            <TabPane tabId="pageHistory">
-              <div className="overflow-auto">
+            {!isGuestUser && (
+              <TabPane tabId="pageHistory">
                 {activeComponents.has('pageHistory') && <PageHistory /> }
                 {activeComponents.has('pageHistory') && <PageHistory /> }
-              </div>
-            </TabPane>
+              </TabPane>
+            )}
             <TabPane tabId="attachment">
             <TabPane tabId="attachment">
               {activeComponents.has('attachment') && <PageAttachment />}
               {activeComponents.has('attachment') && <PageAttachment />}
             </TabPane>
             </TabPane>
-            {!isGuestUserMode && (
+            {!isGuestUser && (
               <TabPane tabId="shareLink">
               <TabPane tabId="shareLink">
                 {activeComponents.has('shareLink') && <ShareLink />}
                 {activeComponents.has('shareLink') && <ShareLink />}
               </TabPane>
               </TabPane>
@@ -134,7 +137,8 @@ const PageAccessoriesModalWrapper = withUnstatedContainers(PageAccessoriesModal,
 PageAccessoriesModal.propTypes = {
 PageAccessoriesModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
-  isGuestUserMode: PropTypes.bool.isRequired,
+  isGuestUser: PropTypes.bool.isRequired,
+  isSharedUser: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func,
   onClose: PropTypes.func,
 };
 };

+ 57 - 52
src/client/js/components/PageAccessoriesModalControl.jsx

@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useMemo } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
@@ -16,61 +16,65 @@ import SeenUserInfo from './User/SeenUserInfo';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 const PageAccessoriesModalControl = (props) => {
 const PageAccessoriesModalControl = (props) => {
-  const { t, pageAccessoriesContainer, isGuestUserMode } = props;
+  const {
+    t, pageAccessoriesContainer, isGuestUser, isSharedUser,
+  } = props;
+
+  const accessoriesBtnList = useMemo(() => {
+    return [
+      {
+        name: 'pagelist',
+        Icon: <PageListIcon />,
+        disabled: isSharedUser,
+      },
+      {
+        name: 'timeline',
+        Icon: <TimeLineIcon />,
+        disabled: isSharedUser,
+      },
+      {
+        name: 'pageHistory',
+        Icon: <HistoryIcon />,
+        disabled: isGuestUser || isSharedUser,
+      },
+      {
+        name: 'attachment',
+        Icon: <AttachmentIcon />,
+        disabled: false,
+      },
+      {
+        name: 'shareLink',
+        Icon: <ShareLinkIcon />,
+        disabled: isGuestUser || isSharedUser,
+      },
+    ];
+  }, [isGuestUser, isSharedUser]);
 
 
   return (
   return (
     <div className="grw-page-accessories-control d-flex align-items-center justify-content-between pb-1">
     <div className="grw-page-accessories-control d-flex align-items-center justify-content-between pb-1">
-
-      <button
-        type="button"
-        className="btn btn-link grw-btn-page-accessories"
-        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pagelist')}
-      >
-        <PageListIcon />
-      </button>
-
-      <button
-        type="button"
-        className="btn btn-link grw-btn-page-accessories"
-        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('timeline')}
-      >
-        <TimeLineIcon />
-      </button>
-
-      <button
-        type="button"
-        className="btn btn-link grw-btn-page-accessories"
-        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pageHistory')}
-      >
-        <HistoryIcon />
-      </button>
-
-      <button
-        type="button"
-        className="btn btn-link grw-btn-page-accessories"
-        onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('attachment')}
-      >
-        <AttachmentIcon />
-      </button>
-
-      <div id="shareLink-btn-wrapper-for-tooltip">
-        <button
-          type="button"
-          className={`btn btn-link grw-btn-page-accessories ${isGuestUserMode && 'disabled'}`}
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('shareLink')}
-        >
-          <ShareLinkIcon />
-        </button>
-      </div>
-      {isGuestUserMode && (
-        <UncontrolledTooltip placement="top" target="shareLink-btn-wrapper-for-tooltip" fade={false}>
-          {t('Not available for guest')}
-        </UncontrolledTooltip>
-      )}
-
+      {accessoriesBtnList.map((accessory) => {
+        return (
+          <>
+            <div id={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`}>
+              <button
+                type="button"
+                className={`btn btn-link grw-btn-page-accessories ${accessory.disabled && 'disabled'}`}
+                onClick={() => pageAccessoriesContainer.openPageAccessoriesModal(accessory.name)}
+              >
+                {accessory.Icon}
+              </button>
+            </div>
+            {accessory.disabled && (
+              <UncontrolledTooltip placement="top" target={`shareLink-btn-wrapper-for-tooltip-for-${accessory.name}`} fade={false}>
+                {t('Not available for guest')}
+              </UncontrolledTooltip>
+            )}
+          </>
+        );
+      })}
       <div className="d-flex align-items-center">
       <div className="d-flex align-items-center">
         <span className="border-left grw-border-vr">&nbsp;</span>
         <span className="border-left grw-border-vr">&nbsp;</span>
-        <SeenUserInfo />
+        <SeenUserInfo disabled={isSharedUser} />
       </div>
       </div>
     </div>
     </div>
   );
   );
@@ -85,7 +89,8 @@ PageAccessoriesModalControl.propTypes = {
 
 
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
 
 
-  isGuestUserMode: PropTypes.bool.isRequired,
+  isGuestUser: PropTypes.bool.isRequired,
+  isSharedUser: PropTypes.bool.isRequired,
 };
 };
 
 
 export default withTranslation()(PageAccessoriesModalControlWrapper);
 export default withTranslation()(PageAccessoriesModalControlWrapper);

+ 22 - 20
src/client/js/components/PageComment/CommentEditor.jsx

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 
 import {
 import {
   Button,
   Button,
-  TabContent, TabPane, Nav, NavItem, NavLink,
+  TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
@@ -21,6 +21,20 @@ import SlackNotification from '../SlackNotification';
 
 
 import CommentPreview from './CommentPreview';
 import CommentPreview from './CommentPreview';
 import NotAvailableForGuest from '../NotAvailableForGuest';
 import NotAvailableForGuest from '../NotAvailableForGuest';
+import { CustomNav } from '../CustomNavigation';
+
+const navTabMapping = {
+  comment_editor: {
+    Icon: () => <i className="icon-settings" />,
+    i18n: 'Write',
+    index: 0,
+  },
+  comment_preview: {
+    Icon: () => <i className="icon-settings" />,
+    i18n: 'Preview',
+    index: 1,
+  },
+};
 
 
 /**
 /**
  *
  *
@@ -43,7 +57,7 @@ class CommentEditor extends React.Component {
       comment: this.props.commentBody || '',
       comment: this.props.commentBody || '',
       isMarkdown: true,
       isMarkdown: true,
       html: '',
       html: '',
-      activeTab: 1,
+      activeTab: 'comment_editor',
       isUploadable,
       isUploadable,
       isUploadableFile,
       isUploadableFile,
       errorMessage: undefined,
       errorMessage: undefined,
@@ -94,7 +108,7 @@ class CommentEditor extends React.Component {
       comment: '',
       comment: '',
       isMarkdown: true,
       isMarkdown: true,
       html: '',
       html: '',
-      activeTab: 1,
+      activeTab: 'comment_editor',
       errorMessage: undefined,
       errorMessage: undefined,
     });
     });
     // reset value
     // reset value
@@ -280,25 +294,13 @@ class CommentEditor extends React.Component {
       </Button>
       </Button>
     );
     );
 
 
+
     return (
     return (
       <>
       <>
         <div className="comment-write">
         <div className="comment-write">
-          <Nav tabs>
-            <NavItem>
-              <NavLink type="button" className={activeTab === 1 ? 'active' : ''} onClick={() => this.handleSelect(1)}>
-                    Write
-              </NavLink>
-            </NavItem>
-            { this.state.isMarkdown && (
-            <NavItem>
-              <NavLink type="button" className={activeTab === 2 ? 'active' : ''} onClick={() => this.handleSelect(2)}>
-                      Preview
-              </NavLink>
-            </NavItem>
-                ) }
-          </Nav>
+          <CustomNav activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={this.handleSelect} hideBorderBottom />
           <TabContent activeTab={activeTab}>
           <TabContent activeTab={activeTab}>
-            <TabPane tabId={1}>
+            <TabPane tabId="comment_editor">
               <Editor
               <Editor
                 ref={(c) => { this.editor = c }}
                 ref={(c) => { this.editor = c }}
                 value={this.state.comment}
                 value={this.state.comment}
@@ -313,7 +315,7 @@ class CommentEditor extends React.Component {
                 onCtrlEnter={this.ctrlEnterHandler}
                 onCtrlEnter={this.ctrlEnterHandler}
               />
               />
             </TabPane>
             </TabPane>
-            <TabPane tabId={2}>
+            <TabPane tabId="comment_preview">
               <div className="comment-form-preview">
               <div className="comment-form-preview">
                 {commentPreview}
                 {commentPreview}
               </div>
               </div>
@@ -324,7 +326,7 @@ class CommentEditor extends React.Component {
         <div className="comment-submit">
         <div className="comment-submit">
           <div className="d-flex">
           <div className="d-flex">
             <label className="mr-2">
             <label className="mr-2">
-              {activeTab === 1 && (
+              {activeTab === 'comment_editor' && (
               <span className="custom-control custom-checkbox">
               <span className="custom-control custom-checkbox">
                 <input
                 <input
                   type="checkbox"
                   type="checkbox"

+ 51 - 76
src/client/js/components/PaginationWrapper.jsx

@@ -1,42 +1,28 @@
-import React from 'react';
+import React, { useCallback, useMemo } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { withTranslation } from 'react-i18next';
-
 import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
 import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
 
 
-class PaginationWrapper extends React.Component {
-
-  constructor(props) {
-    super(props);
+/**
+ *
+ * @author Mikitaka Itizawa <itizawa@weseek.co.jp>
+ *
+ * @export
+ * @class PaginationWrapper
+ * @extends {React.Component}
+ */
 
 
-    this.state = {
-      activePage: 1,
-      totalItemsCount: 0,
-      paginationNumbers: {},
-      limit: this.props.pagingLimit || Infinity,
-    };
+const PaginationWrapper = React.memo((props) => {
+  const {
+    activePage, changePage, totalItemsCount, pagingLimit, align,
+  } = props;
 
 
-    this.calculatePagination = this.calculatePagination.bind(this);
-  }
-
-  componentWillReceiveProps(nextProps) {
-    this.setState({
-      activePage: nextProps.activePage,
-      totalItemsCount: nextProps.totalItemsCount,
-      limit: nextProps.pagingLimit,
-    }, () => {
-      const activePage = this.state.activePage;
-      const totalCount = this.state.totalItemsCount;
-      const limit = this.state.limit;
-      const paginationNumbers = this.calculatePagination(limit, totalCount, activePage);
-      this.setState({ paginationNumbers });
-    });
-  }
-
-  calculatePagination(limit, totalCount, activePage) {
+  /**
+   * various numbers used to generate pagination dom
+   */
+  const paginationNumbers = useMemo(() => {
     // calc totalPageNumber
     // calc totalPageNumber
-    const totalPage = Math.floor(totalCount / limit) + (totalCount % limit === 0 ? 0 : 1);
+    const totalPage = Math.floor(totalItemsCount / pagingLimit) + (totalItemsCount % pagingLimit === 0 ? 0 : 1);
 
 
     let paginationStart = activePage - 2;
     let paginationStart = activePage - 2;
     let maxViewPageNum = activePage + 2;
     let maxViewPageNum = activePage + 2;
@@ -58,22 +44,26 @@ class PaginationWrapper extends React.Component {
       paginationStart,
       paginationStart,
       maxViewPageNum,
       maxViewPageNum,
     };
     };
-  }
+  }, [activePage, totalItemsCount, pagingLimit]);
+
+  const { paginationStart } = paginationNumbers;
+  const { maxViewPageNum } = paginationNumbers;
+  const { totalPage } = paginationNumbers;
 
 
   /**
   /**
-    * generate Elements of Pagination First Prev
-    * ex.  <<   <   1  2  3  >  >>
-    * this function set << & <
-    */
-  generateFirstPrev(activePage) {
+   * generate Elements of Pagination First Prev
+   * ex.  <<   <   1  2  3  >  >>
+   * this function set << & <
+   */
+  const generateFirstPrev = useCallback(() => {
     const paginationItems = [];
     const paginationItems = [];
     if (activePage !== 1) {
     if (activePage !== 1) {
       paginationItems.push(
       paginationItems.push(
         <PaginationItem key="painationItemFirst">
         <PaginationItem key="painationItemFirst">
-          <PaginationLink first onClick={() => { return this.props.changePage(1) }} />
+          <PaginationLink first onClick={() => { return changePage(1) }} />
         </PaginationItem>,
         </PaginationItem>,
         <PaginationItem key="painationItemPrevious">
         <PaginationItem key="painationItemPrevious">
-          <PaginationLink previous onClick={() => { return this.props.changePage(activePage - 1) }} />
+          <PaginationLink previous onClick={() => { return changePage(activePage - 1) }} />
         </PaginationItem>,
         </PaginationItem>,
       );
       );
     }
     }
@@ -88,41 +78,41 @@ class PaginationWrapper extends React.Component {
       );
       );
     }
     }
     return paginationItems;
     return paginationItems;
-  }
+  }, [activePage, changePage]);
 
 
   /**
   /**
    * generate Elements of Pagination First Prev
    * generate Elements of Pagination First Prev
    *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
    *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
    * this function set  numbers
    * this function set  numbers
    */
    */
-  generatePaginations(activePage, paginationStart, maxViewPageNum) {
+  const generatePaginations = useCallback(() => {
     const paginationItems = [];
     const paginationItems = [];
     for (let number = paginationStart; number <= maxViewPageNum; number++) {
     for (let number = paginationStart; number <= maxViewPageNum; number++) {
       paginationItems.push(
       paginationItems.push(
         <PaginationItem key={`paginationItem-${number}`} active={number === activePage}>
         <PaginationItem key={`paginationItem-${number}`} active={number === activePage}>
-          <PaginationLink onClick={() => { return this.props.changePage(number) }}>
+          <PaginationLink onClick={() => { return changePage(number) }}>
             {number}
             {number}
           </PaginationLink>
           </PaginationLink>
         </PaginationItem>,
         </PaginationItem>,
       );
       );
     }
     }
     return paginationItems;
     return paginationItems;
-  }
+  }, [activePage, changePage, paginationStart, maxViewPageNum]);
 
 
   /**
   /**
    * generate Elements of Pagination First Prev
    * generate Elements of Pagination First Prev
    * ex.  <<   <   1  2  3  >  >>
    * ex.  <<   <   1  2  3  >  >>
    * this function set > & >>
    * this function set > & >>
    */
    */
-  generateNextLast(activePage, totalPage) {
+  const generateNextLast = useCallback(() => {
     const paginationItems = [];
     const paginationItems = [];
     if (totalPage !== activePage) {
     if (totalPage !== activePage) {
       paginationItems.push(
       paginationItems.push(
         <PaginationItem key="painationItemNext">
         <PaginationItem key="painationItemNext">
-          <PaginationLink next onClick={() => { return this.props.changePage(activePage + 1) }} />
+          <PaginationLink next onClick={() => { return changePage(activePage + 1) }} />
         </PaginationItem>,
         </PaginationItem>,
         <PaginationItem key="painationItemLast">
         <PaginationItem key="painationItemLast">
-          <PaginationLink last onClick={() => { return this.props.changePage(totalPage) }} />
+          <PaginationLink last onClick={() => { return changePage(totalPage) }} />
         </PaginationItem>,
         </PaginationItem>,
       );
       );
     }
     }
@@ -137,13 +127,11 @@ class PaginationWrapper extends React.Component {
       );
       );
     }
     }
     return paginationItems;
     return paginationItems;
+  }, [activePage, changePage, totalPage]);
 
 
-  }
-
-  getListClassName() {
+  const getListClassName = useMemo(() => {
     const listClassNames = [];
     const listClassNames = [];
 
 
-    const { align } = this.props;
     if (align === 'center') {
     if (align === 'center') {
       listClassNames.push('justify-content-center');
       listClassNames.push('justify-content-center');
     }
     }
@@ -152,34 +140,21 @@ class PaginationWrapper extends React.Component {
     }
     }
 
 
     return listClassNames.join(' ');
     return listClassNames.join(' ');
-  }
-
-  render() {
-    const paginationItems = [];
+  }, [align]);
 
 
-    const activePage = this.state.activePage;
-    const totalPage = this.state.paginationNumbers.totalPage;
-    const paginationStart = this.state.paginationNumbers.paginationStart;
-    const maxViewPageNum = this.state.paginationNumbers.maxViewPageNum;
-    const firstPrevItems = this.generateFirstPrev(activePage);
-    paginationItems.push(firstPrevItems);
-    const paginations = this.generatePaginations(activePage, paginationStart, maxViewPageNum);
-    paginationItems.push(paginations);
-    const nextLastItems = this.generateNextLast(activePage, totalPage);
-    paginationItems.push(nextLastItems);
+  return (
+    <React.Fragment>
+      <Pagination size={props.size} listClassName={getListClassName}>
+        {generateFirstPrev()}
+        {generatePaginations()}
+        {generateNextLast()}
+      </Pagination>
+    </React.Fragment>
+  );
 
 
-    return (
-      <React.Fragment>
-        <Pagination size={this.props.size} listClassName={this.getListClassName()}>{paginationItems}</Pagination>
-      </React.Fragment>
-    );
-  }
-
-
-}
+});
 
 
 PaginationWrapper.propTypes = {
 PaginationWrapper.propTypes = {
-
   activePage: PropTypes.number.isRequired,
   activePage: PropTypes.number.isRequired,
   changePage: PropTypes.func.isRequired,
   changePage: PropTypes.func.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
@@ -193,4 +168,4 @@ PaginationWrapper.defaultProps = {
   size: 'md',
   size: 'md',
 };
 };
 
 
-export default withTranslation()(PaginationWrapper);
+export default PaginationWrapper;

+ 3 - 2
src/client/js/components/User/SeenUserInfo.jsx

@@ -18,14 +18,14 @@ import FootstampIcon from '../FootstampIcon';
 const SeenUserInfo = (props) => {
 const SeenUserInfo = (props) => {
   const [popoverOpen, setPopoverOpen] = useState(false);
   const [popoverOpen, setPopoverOpen] = useState(false);
   const toggle = () => setPopoverOpen(!popoverOpen);
   const toggle = () => setPopoverOpen(!popoverOpen);
-  const { pageContainer } = props;
+  const { pageContainer, disabled } = props;
   return (
   return (
     <div className="grw-seen-user-info">
     <div className="grw-seen-user-info">
       <Button id="po-seen-user" color="link" className="px-2">
       <Button id="po-seen-user" color="link" className="px-2">
         <span className="mr-1 footstamp-icon"><FootstampIcon /></span>
         <span className="mr-1 footstamp-icon"><FootstampIcon /></span>
         <span className="seen-user-count">{pageContainer.state.countOfSeenUsers}</span>
         <span className="seen-user-count">{pageContainer.state.countOfSeenUsers}</span>
       </Button>
       </Button>
-      <Popover placement="bottom" isOpen={popoverOpen} target="po-seen-user" toggle={toggle} trigger="legacy">
+      <Popover placement="bottom" isOpen={popoverOpen} target="po-seen-user" toggle={toggle} trigger="legacy" disabled={disabled}>
         <PopoverBody className="seen-user-popover">
         <PopoverBody className="seen-user-popover">
           <div className="px-2 text-right user-list-content text-truncate text-muted">
           <div className="px-2 text-right user-list-content text-truncate text-muted">
             <UserPictureList users={pageContainer.state.seenUsers} />
             <UserPictureList users={pageContainer.state.seenUsers} />
@@ -38,6 +38,7 @@ const SeenUserInfo = (props) => {
 
 
 SeenUserInfo.propTypes = {
 SeenUserInfo.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+  disabled: PropTypes.bool,
 };
 };
 
 
 /**
 /**

+ 2 - 2
src/client/js/legacy/crowi.js

@@ -157,13 +157,13 @@ Crowi.highlightSelectedSection = function(hash) {
 window.addEventListener('load', (e) => {
 window.addEventListener('load', (e) => {
   const { appContainer } = window;
   const { appContainer } = window;
   const pageContainer = appContainer.getContainer('PageContainer');
   const pageContainer = appContainer.getContainer('PageContainer');
-  const { isEditable } = pageContainer;
+  const { isAbleToOpenPageEditor } = pageContainer;
 
 
   // hash on page
   // hash on page
   if (window.location.hash) {
   if (window.location.hash) {
     const navigationContainer = appContainer.getContainer('NavigationContainer');
     const navigationContainer = appContainer.getContainer('NavigationContainer');
 
 
-    if (window.location.hash === '#edit' && isEditable) {
+    if (window.location.hash === '#edit' && isAbleToOpenPageEditor) {
       navigationContainer.setEditorMode('edit');
       navigationContainer.setEditorMode('edit');
 
 
       // focus
       // focus

+ 6 - 3
src/client/js/services/AppContainer.js

@@ -39,9 +39,6 @@ export default class AppContainer extends Container {
 
 
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
     this.config = JSON.parse(document.getElementById('growi-context-hydrate').textContent || '{}');
 
 
-    const isSharedPageElem = document.getElementById('is-shared-page');
-    this.isSharedUser = (isSharedPageElem != null);
-
     const userAgent = window.navigator.userAgent.toLowerCase();
     const userAgent = window.navigator.userAgent.toLowerCase();
     this.isMobile = /iphone|ipad|android/.test(userAgent);
     this.isMobile = /iphone|ipad|android/.test(userAgent);
 
 
@@ -50,6 +47,12 @@ export default class AppContainer extends Container {
       this.currentUser = JSON.parse(currentUserElem.textContent);
       this.currentUser = JSON.parse(currentUserElem.textContent);
     }
     }
 
 
+    const isSharedPageElem = document.getElementById('is-shared-page');
+
+    // check what kind of user
+    this.isGuestUser = this.currentUser == null;
+    this.isSharedUser = isSharedPageElem != null && this.currentUser == null;
+
     const userLocaleId = this.currentUser?.lang;
     const userLocaleId = this.currentUser?.lang;
     this.i18n = i18nFactory(userLocaleId);
     this.i18n = i18nFactory(userLocaleId);
 
 

+ 21 - 11
src/client/js/services/PageContainer.js

@@ -60,12 +60,15 @@ export default class PageContainer extends Container {
       sumOfBookmarks: 0,
       sumOfBookmarks: 0,
       createdAt: mainContent.getAttribute('data-page-created-at'),
       createdAt: mainContent.getAttribute('data-page-created-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
+
       isTrashPage: isTrashPage(path),
       isTrashPage: isTrashPage(path),
       isForbidden: JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isForbidden: JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isDeleted: JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
       isDeleted: JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
       isDeletable: JSON.parse(mainContent.getAttribute('data-page-is-deletable')),
       isDeletable: JSON.parse(mainContent.getAttribute('data-page-is-deletable')),
       isNotCreatable: JSON.parse(mainContent.getAttribute('data-page-is-not-creatable')),
       isNotCreatable: JSON.parse(mainContent.getAttribute('data-page-is-not-creatable')),
       isAbleToDeleteCompletely: JSON.parse(mainContent.getAttribute('data-page-is-able-to-delete-completely')),
       isAbleToDeleteCompletely: JSON.parse(mainContent.getAttribute('data-page-is-able-to-delete-completely')),
+      isPageExist: mainContent.getAttribute('data-page-id') != null,
+
       pageUser: JSON.parse(mainContent.getAttribute('data-page-user')),
       pageUser: JSON.parse(mainContent.getAttribute('data-page-user')),
       tags: null,
       tags: null,
       hasChildren: JSON.parse(mainContent.getAttribute('data-page-has-children')),
       hasChildren: JSON.parse(mainContent.getAttribute('data-page-has-children')),
@@ -104,9 +107,10 @@ export default class PageContainer extends Container {
     this.initStateMarkdown();
     this.initStateMarkdown();
     this.checkAndUpdateImageUrlCached(this.state.likerUsers);
     this.checkAndUpdateImageUrlCached(this.state.likerUsers);
 
 
-    const { currentUser } = this.appContainer;
+    const { isSharedUser } = this.appContainer;
+
     // see https://dev.growi.org/5fabddf8bbeb1a0048bcb9e9
     // see https://dev.growi.org/5fabddf8bbeb1a0048bcb9e9
-    const isAbleToGetAttachedInformationAboutPages = this.state.pageId != null || !(currentUser == null && this.state.isSharedPage);
+    const isAbleToGetAttachedInformationAboutPages = this.state.isPageExist && !isSharedUser;
 
 
     if (isAbleToGetAttachedInformationAboutPages) {
     if (isAbleToGetAttachedInformationAboutPages) {
       this.retrieveSeenUsers();
       this.retrieveSeenUsers();
@@ -143,16 +147,22 @@ export default class PageContainer extends Container {
   }
   }
 
 
 
 
-  get isEditable() {
-    const { currentUser } = this.appContainer;
-    const {
-      isPageExist, isPageForbidden, isNotCreatable, isTrashPage,
-    } = this.state;
+  get isAbleToOpenPageEditor() {
+    const { isPageForbidden, isNotCreatable, isTrashPage } = this.state;
+    const { isGuestUser } = this.appContainer;
 
 
-    if (isPageExist && (currentUser != null) && !isPageForbidden && !isNotCreatable && !isTrashPage) {
-      return true;
-    }
-    return false;
+    return (!isGuestUser && !isPageForbidden && !isNotCreatable && !isTrashPage);
+  }
+
+  /**
+   * whether to display reaction buttons
+   * ex.) like, bookmark
+   */
+  get isAbleToShowPageReactionButtons() {
+    const { isTrashPage, isPageExist } = this.state;
+    const { isSharedUser } = this.appContainer;
+
+    return (!isTrashPage && isPageExist && !isSharedUser);
   }
   }
 
 
   /**
   /**

+ 28 - 0
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -25,6 +25,34 @@ $gray-900: darken($dark, 5%) !default;
 $grays: ("50": $gray-50) !default;
 $grays: ("50": $gray-50) !default;
 $red: #ff0a54 !default;
 $red: #ff0a54 !default;
 
 
+
+// Grid breakpoints
+//
+// Define the minimum dimensions at which your layout will change,
+// adapting to different screen sizes, for use in media queries.
+
+$grid-breakpoints: (
+  xs: 0,
+  sm: 576px,
+  md: 768px,
+  lg: 992px,
+  xl: 1200px,
+  2xl: 1480px
+);
+
+// Grid containers
+//
+// Define the maximum width of `.container` for different screen sizes.
+
+$container-max-widths: (
+  sm: 540px,
+  md: 720px,
+  lg: 960px,
+  xl: 1140px,
+  2xl: 1320px
+);
+
+
 //== Typography
 //== Typography
 //
 //
 //## Font, line-height, and color for body text, headings, and more.
 //## Font, line-height, and color for body text, headings, and more.

+ 5 - 2
src/client/styles/scss/theme/_apply-colors.scss

@@ -39,6 +39,9 @@ $nav-tabs-link-active-border-color: $bordercolor-nav-tabs-active;
 @import 'reboot-bootstrap-nav';
 @import 'reboot-bootstrap-nav';
 @import 'reboot-toastr-colors';
 @import 'reboot-toastr-colors';
 
 
+// determine variables with bootstrap function (These variables can be used after importing bootstrap above)
+$color-modal-header: color-yiq($primary) !default;
+
 :not(pre) {
 :not(pre) {
   > code {
   > code {
     color: $color-inline-code;
     color: $color-inline-code;
@@ -286,10 +289,10 @@ ul.pagination {
   .modal-header {
   .modal-header {
     border-bottom-color: $border-color-theme;
     border-bottom-color: $border-color-theme;
     .modal-title {
     .modal-title {
-      color: color-yiq($primary);
+      color: $color-modal-header;
     }
     }
     .close {
     .close {
-      color: color-yiq($primary);
+      color: $color-modal-header;
       opacity: 0.5;
       opacity: 0.5;
       &:hover {
       &:hover {
         opacity: 0.9;
         opacity: 0.9;

+ 1 - 0
src/client/styles/scss/theme/christmas.scss

@@ -51,6 +51,7 @@ html[dark] {
   $color-link-hover: lighten($color-link, 10%);
   $color-link-hover: lighten($color-link, 10%);
   $color-link-nabvar: $color-reversal;
   $color-link-nabvar: $color-reversal;
   $color-inline-code: #c7254e; // optional
   $color-inline-code: #c7254e; // optional
+  $color-modal-header: $themelight;
 
 
   // Table colors
   // Table colors
   $border-color-table: $gray-400; // optional
   $border-color-table: $gray-400; // optional