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

Merge branch 'master' into fix/4302-4393-dont-push-like-page-at-share-link-page

itizawa 5 лет назад
Родитель
Сommit
4937476743

+ 2 - 2
src/client/js/components/Admin/UserGroup/UserGroupDeleteModal.jsx

@@ -100,8 +100,8 @@ class UserGroupDeleteModal extends React.Component {
     const { t } = this.props;
 
     const optoins = this.availableOptions.map((opt) => {
-      const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${t(opt.label)}</span>`;
-      return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{t(opt.label)}</option>;
+      const dataContent = `<i class="icon icon-fw ${opt.iconClass} ${opt.styleClass}"></i> <span class="action-name ${opt.styleClass}">${opt.label}</span>`;
+      return <option key={opt.id} value={opt.actionForPages} data-content={dataContent}>{opt.label}</option>;
     });
 
     return (

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

@@ -152,13 +152,16 @@ class UserGroupPage extends React.Component {
           onDelete={this.showDeleteModal}
           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
           userGroups={this.state.userGroups}
           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">
           {this.state.currentPages.map(page => <li key={page._id}><Page page={page} /></li>)}
         </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>
     );
   }

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

@@ -131,9 +131,9 @@ class UserInviteModal extends React.Component {
         {userList.map((user) => {
           const copyText = `Email:${user.email} Password:${user.password} `;
           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>
                 </li>
               </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 { UncontrolledTooltip } from 'reactstrap';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import NavigationContainer from '../../services/NavigationContainer';
 import AppContainer from '../../services/AppContainer';
 
+
 import GrowiLogo from '../Icons/GrowiLogo';
 
 import PersonalDropdown from './PersonalDropdown';
@@ -45,10 +47,18 @@ class GrowiNavbar extends React.Component {
 
     return (
       <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">
           {crowi.confidential}
         </span>
+        <UncontrolledTooltip
+          placement="bottom"
+          trigger="click"
+          target="confidentialTooltip"
+          className="d-md-none"
+        >
+          {crowi.confidential}
+        </UncontrolledTooltip>
       </li>
     );
   }

+ 22 - 13
src/client/js/components/Page/CopyDropdown.jsx

@@ -16,25 +16,24 @@ class CopyDropdown extends React.Component {
     super(props);
 
     this.state = {
-      dropdownOpen: false,
       tooltipOpen: false,
       isParamsAppended: true,
+      pagePathWithParams: '',
+      pagePathUrl: '',
+      permalink: '',
+      markdownLink: '',
     };
 
     this.id = (Math.random() * 1000).toString();
 
-    this.toggle = this.toggle.bind(this);
     this.showToolTip = this.showToolTip.bind(this);
+    this.generateItemContents = this.generateItemContents.bind(this);
     this.generatePagePathWithParams = this.generatePagePathWithParams.bind(this);
     this.generatePagePathUrl = this.generatePagePathUrl.bind(this);
     this.generatePermalink = this.generatePermalink.bind(this);
     this.generateMarkdownLink = this.generateMarkdownLink.bind(this);
   }
 
-  toggle() {
-    this.setState({ dropdownOpen: !this.state.dropdownOpen });
-  }
-
   showToolTip() {
     this.setState({ tooltipOpen: true });
     setTimeout(() => {
@@ -64,6 +63,17 @@ class CopyDropdown extends React.Component {
     return str.replace(/ /g, '%20').replace(/\u3000/g, '%E3%80%80');
   }
 
+  generateItemContents() {
+    const pagePathWithParams = this.generatePagePathWithParams();
+    const pagePathUrl = this.generatePagePathUrl();
+    const permalink = this.generatePermalink();
+    const markdownLink = this.generateMarkdownLink();
+
+    this.setState({
+      pagePathWithParams, pagePathUrl, permalink, markdownLink,
+    });
+  }
+
   generatePagePathWithParams() {
     const { pagePath } = this.props;
     return decodeURI(`${pagePath}${this.uriParams}`);
@@ -108,11 +118,9 @@ class CopyDropdown extends React.Component {
     const {
       t, pageId, isShareLinkMode,
     } = this.props;
-    const { isParamsAppended } = this.state;
-
-    const pagePathWithParams = this.generatePagePathWithParams();
-    const pagePathUrl = this.generatePagePathUrl();
-    const permalink = this.generatePermalink();
+    const {
+      isParamsAppended, pagePathWithParams, pagePathUrl, permalink, markdownLink,
+    } = this.state;
 
     const copyTarget = isShareLinkMode ? `copyShareLink${pageId}` : 'copyPagePathDropdown';
     const dropdownToggleStyle = isShareLinkMode ? 'btn btn-secondary' : 'd-block text-muted bg-transparent btn-copy border-0';
@@ -128,6 +136,7 @@ class CopyDropdown extends React.Component {
             caret
             className={dropdownToggleStyle}
             style={this.props.buttonStyle}
+            onClick={this.generateItemContents}
           >
             { isShareLinkMode ? (
               <>Copy Link</>
@@ -195,9 +204,9 @@ class CopyDropdown extends React.Component {
 
             {/* Markdown Link */}
             { pageId && (
-              <CopyToClipboard text={this.generateMarkdownLink()} onCopy={this.showToolTip}>
+              <CopyToClipboard text={markdownLink} onCopy={this.showToolTip}>
                 <DropdownItem className="px-3 text-wrap">
-                  <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={this.generateMarkdownLink()} isContentsWrap />
+                  <DropdownItemContents title={t('copy_to_clipboard.Markdown link')} contents={markdownLink} isContentsWrap />
                 </DropdownItem>
               </CopyToClipboard>
             )}

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

@@ -28,15 +28,18 @@ const NotFoundAlert = (props) => {
           {t('not_found_page.page_not_exist_alert')}
         </h2>
         <button
-          id="create-page-btn-wrapper-for-tooltip"
           type="button"
-          className={`m-1 pl-3 pr-3 btn bg-info text-white ${isGuestUserMode && 'disabled'}`}
+          className="m-1 pl-3 pr-3 btn bg-info text-white"
           onClick={() => { clickHandler('edit') }}
+          disabled={isGuestUserMode}
         >
-          <i className="icon-note icon-fw" />
-          {t('not_found_page.Create Page')}
+          <div id="create-page-btn-wrapper-for-tooltip">
+            <i className="icon-note icon-fw" />
+            {t('not_found_page.Create Page')}
+          </div>
         </button>
 
+
         {isGuestUserMode && (
         <UncontrolledTooltip placement="bottom" target="create-page-btn-wrapper-for-tooltip" fade={false}>
           {t('Not available for guest')}

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

@@ -10,18 +10,15 @@ import PageAccessoriesContainer from '../services/PageAccessoriesContainer';
 
 const PageAccessories = (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 = appContainer.currentUser == null;
+  const isSharedUser = appContainer.isSharedUser;
 
   return (
     <>
-      <PageAccessoriesModalControl isGuestUserMode={isGuestUserMode} />
+      <PageAccessoriesModalControl isGuestUser={isGuestUser} isSharedUser={isSharedUser} />
       <PageAccessoriesModal
-        isGuestUserMode={isGuestUserMode}
+        isGuestUser={isGuestUser}
+        isSharedUser={isSharedUser}
         isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
         onClose={pageAccessoriesContainer.closePageAccessoriesModal}
       />

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

@@ -24,7 +24,7 @@ import ExpandOrContractButton from './ExpandOrContractButton';
 
 const PageAccessoriesModal = (props) => {
   const {
-    t, pageAccessoriesContainer, onClose, isGuestUserMode,
+    t, pageAccessoriesContainer, onClose, isGuestUser, isSharedUser,
   } = props;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { activeTab, activeComponents } = pageAccessoriesContainer.state;
@@ -36,16 +36,19 @@ const PageAccessoriesModal = (props) => {
         Icon: PageListIcon,
         i18n: t('page_list'),
         index: 0,
+        isLinkEnabled: v => !isSharedUser,
       },
-      timeline:  {
+      timeline: {
         Icon: TimeLineIcon,
         i18n: t('Timeline View'),
         index: 1,
+        isLinkEnabled: v => !isSharedUser,
       },
       pageHistory: {
         Icon: HistoryIcon,
         i18n: t('History'),
         index: 2,
+        isLinkEnabled: v => !isSharedUser,
       },
       attachment: {
         Icon: AttachmentIcon,
@@ -56,10 +59,10 @@ const PageAccessoriesModal = (props) => {
         Icon: ShareLinkIcon,
         i18n: t('share_links.share_link_management'),
         index: 4,
-        isLinkEnabled: v => !isGuestUserMode,
+        isLinkEnabled: v => !isGuestUser || !isSharedUser,
       },
     };
-  }, [t, isGuestUserMode]);
+  }, [t, isGuestUser, isSharedUser]);
 
   const closeModalHandler = useCallback(() => {
     if (onClose == null) {
@@ -114,7 +117,7 @@ const PageAccessoriesModal = (props) => {
             <TabPane tabId="attachment">
               {activeComponents.has('attachment') && <PageAttachment />}
             </TabPane>
-            {!isGuestUserMode && (
+            {!isGuestUser && (
               <TabPane tabId="shareLink">
                 {activeComponents.has('shareLink') && <ShareLink />}
               </TabPane>
@@ -134,7 +137,8 @@ const PageAccessoriesModalWrapper = withUnstatedContainers(PageAccessoriesModal,
 PageAccessoriesModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
-  isGuestUserMode: PropTypes.bool.isRequired,
+  isGuestUser: PropTypes.bool.isRequired,
+  isSharedUser: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   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 { withTranslation } from 'react-i18next';
@@ -16,61 +16,65 @@ import SeenUserInfo from './User/SeenUserInfo';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 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: isSharedUser,
+      },
+      {
+        name: 'attachment',
+        Icon: <AttachmentIcon />,
+        disabled: false,
+      },
+      {
+        name: 'shareLink',
+        Icon: <ShareLinkIcon />,
+        disabled: isGuestUser || isSharedUser,
+      },
+    ];
+  }, [isGuestUser, isSharedUser]);
 
   return (
     <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">
         <span className="border-left grw-border-vr">&nbsp;</span>
-        <SeenUserInfo />
+        <SeenUserInfo disabled={isSharedUser} />
       </div>
     </div>
   );
@@ -85,7 +89,8 @@ PageAccessoriesModalControl.propTypes = {
 
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
 
-  isGuestUserMode: PropTypes.bool.isRequired,
+  isGuestUser: PropTypes.bool.isRequired,
+  isSharedUser: PropTypes.bool.isRequired,
 };
 
 export default withTranslation()(PageAccessoriesModalControlWrapper);

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

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 
 import {
   Button,
-  TabContent, TabPane, Nav, NavItem, NavLink,
+  TabContent, TabPane,
 } from 'reactstrap';
 
 import * as toastr from 'toastr';
@@ -21,6 +21,20 @@ import SlackNotification from '../SlackNotification';
 
 import CommentPreview from './CommentPreview';
 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 || '',
       isMarkdown: true,
       html: '',
-      activeTab: 1,
+      activeTab: 'comment_editor',
       isUploadable,
       isUploadableFile,
       errorMessage: undefined,
@@ -94,7 +108,7 @@ class CommentEditor extends React.Component {
       comment: '',
       isMarkdown: true,
       html: '',
-      activeTab: 1,
+      activeTab: 'comment_editor',
       errorMessage: undefined,
     });
     // reset value
@@ -280,25 +294,13 @@ class CommentEditor extends React.Component {
       </Button>
     );
 
+
     return (
       <>
         <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}>
-            <TabPane tabId={1}>
+            <TabPane tabId="comment_editor">
               <Editor
                 ref={(c) => { this.editor = c }}
                 value={this.state.comment}
@@ -313,7 +315,7 @@ class CommentEditor extends React.Component {
                 onCtrlEnter={this.ctrlEnterHandler}
               />
             </TabPane>
-            <TabPane tabId={2}>
+            <TabPane tabId="comment_preview">
               <div className="comment-form-preview">
                 {commentPreview}
               </div>
@@ -324,7 +326,7 @@ class CommentEditor extends React.Component {
         <div className="comment-submit">
           <div className="d-flex">
             <label className="mr-2">
-              {activeTab === 1 && (
+              {activeTab === 'comment_editor' && (
               <span className="custom-control custom-checkbox">
                 <input
                   type="checkbox"

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

@@ -115,7 +115,7 @@ const EditorNavbarBottom = (props) => {
         <Collapse isOpen={isExpanded}>
           <div className="px-2"> {/* set padding for border-top */}
             <div className={`navbar navbar-expand border-top px-0 ${additionalClasses.join(' ')}`}>
-              <form className="form-inline ml-auto">
+              <form className="form-inline ml-md-auto">
                 <OptionsSelector />
               </form>
             </div>

+ 3 - 3
src/client/js/components/PageEditor/OptionsSelector.jsx

@@ -247,9 +247,9 @@ class OptionsSelector extends React.Component {
   render() {
     return (
       <div className="d-flex flex-row">
-        <span className="ml-3">{this.renderThemeSelector()}</span>
-        <span className="ml-4">{this.renderKeymapModeSelector()}</span>
-        <span className="ml-4">{this.renderConfigurationDropdown()}</span>
+        <span className="ml-sm-3">{this.renderThemeSelector()}</span>
+        <span className="ml-2 ml-sm-4">{this.renderKeymapModeSelector()}</span>
+        <span className="ml-2 ml-sm-4">{this.renderConfigurationDropdown()}</span>
       </div>
     );
   }

+ 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 { withTranslation } from 'react-i18next';
-
 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
-    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 maxViewPageNum = activePage + 2;
@@ -58,22 +44,26 @@ class PaginationWrapper extends React.Component {
       paginationStart,
       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 = [];
     if (activePage !== 1) {
       paginationItems.push(
         <PaginationItem key="painationItemFirst">
-          <PaginationLink first onClick={() => { return this.props.changePage(1) }} />
+          <PaginationLink first onClick={() => { return changePage(1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemPrevious">
-          <PaginationLink previous onClick={() => { return this.props.changePage(activePage - 1) }} />
+          <PaginationLink previous onClick={() => { return changePage(activePage - 1) }} />
         </PaginationItem>,
       );
     }
@@ -88,41 +78,41 @@ class PaginationWrapper extends React.Component {
       );
     }
     return paginationItems;
-  }
+  }, [activePage, changePage]);
 
   /**
    * generate Elements of Pagination First Prev
    *  ex. << < 4 5 6 7 8 > >>, << < 1 2 3 4 > >>
    * this function set  numbers
    */
-  generatePaginations(activePage, paginationStart, maxViewPageNum) {
+  const generatePaginations = useCallback(() => {
     const paginationItems = [];
     for (let number = paginationStart; number <= maxViewPageNum; number++) {
       paginationItems.push(
         <PaginationItem key={`paginationItem-${number}`} active={number === activePage}>
-          <PaginationLink onClick={() => { return this.props.changePage(number) }}>
+          <PaginationLink onClick={() => { return changePage(number) }}>
             {number}
           </PaginationLink>
         </PaginationItem>,
       );
     }
     return paginationItems;
-  }
+  }, [activePage, changePage, paginationStart, maxViewPageNum]);
 
   /**
    * generate Elements of Pagination First Prev
    * ex.  <<   <   1  2  3  >  >>
    * this function set > & >>
    */
-  generateNextLast(activePage, totalPage) {
+  const generateNextLast = useCallback(() => {
     const paginationItems = [];
     if (totalPage !== activePage) {
       paginationItems.push(
         <PaginationItem key="painationItemNext">
-          <PaginationLink next onClick={() => { return this.props.changePage(activePage + 1) }} />
+          <PaginationLink next onClick={() => { return changePage(activePage + 1) }} />
         </PaginationItem>,
         <PaginationItem key="painationItemLast">
-          <PaginationLink last onClick={() => { return this.props.changePage(totalPage) }} />
+          <PaginationLink last onClick={() => { return changePage(totalPage) }} />
         </PaginationItem>,
       );
     }
@@ -137,13 +127,11 @@ class PaginationWrapper extends React.Component {
       );
     }
     return paginationItems;
+  }, [activePage, changePage, totalPage]);
 
-  }
-
-  getListClassName() {
+  const getListClassName = useMemo(() => {
     const listClassNames = [];
 
-    const { align } = this.props;
     if (align === 'center') {
       listClassNames.push('justify-content-center');
     }
@@ -152,34 +140,21 @@ class PaginationWrapper extends React.Component {
     }
 
     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 = {
-
   activePage: PropTypes.number.isRequired,
   changePage: PropTypes.func.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
@@ -193,4 +168,4 @@ PaginationWrapper.defaultProps = {
   size: 'md',
 };
 
-export default withTranslation()(PaginationWrapper);
+export default PaginationWrapper;

+ 1 - 1
src/client/js/components/RecentCreated/RecentCreated.jsx

@@ -67,7 +67,7 @@ class RecentCreated extends React.Component {
 
     return (
       <div className="page-list-container-create">
-        <ul className="page-list-ul page-list-ul-flat">
+        <ul className="page-list-ul page-list-ul-flat mb-3">
           {pageList}
         </ul>
         <PaginationWrapper

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

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

+ 3 - 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 || '{}');
 
-    const isSharedPageElem = document.getElementById('is-shared-page');
-    this.isSharedUser = (isSharedPageElem != null);
-
     const userAgent = window.navigator.userAgent.toLowerCase();
     this.isMobile = /iphone|ipad|android/.test(userAgent);
 
@@ -50,6 +47,9 @@ export default class AppContainer extends Container {
       this.currentUser = JSON.parse(currentUserElem.textContent);
     }
 
+    const isSharedPageElem = document.getElementById('is-shared-page');
+    this.isSharedUser = this.currentUser == null && isSharedPageElem != null;
+
     const userLocaleId = this.currentUser?.lang;
     this.i18n = i18nFactory(userLocaleId);
 

+ 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;
 $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
 //
 //## Font, line-height, and color for body text, headings, and more.

+ 3 - 0
src/server/views/widget/not_found_content.html

@@ -1,6 +1,9 @@
 <div id="content-main" class="content-main page-list"
   data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
+  data-page-grant="{{ grant }}"
+  data-page-grant-group="{{ grantedGroupId }}"
+  data-page-grant-group-name="{{ grantedGroupName }}"
   {% if templateTags %}
     data-template-tags="{{ templateTags }}"
   {% endif %}