Browse Source

Merge branch 'master' into fix/dropdown-layout-in-User-Management

zamis 5 years ago
parent
commit
b1a57b2e92

+ 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')}

+ 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

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

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

+ 6 - 8
src/client/js/services/PageContainer.js

@@ -60,12 +60,15 @@ export default class PageContainer extends Container {
       sumOfBookmarks: 0,
       createdAt: mainContent.getAttribute('data-page-created-at'),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
+
       isTrashPage: isTrashPage(path),
       isForbidden: JSON.parse(mainContent.getAttribute('data-page-is-forbidden')),
       isDeleted: JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
       isDeletable: JSON.parse(mainContent.getAttribute('data-page-is-deletable')),
       isNotCreatable: JSON.parse(mainContent.getAttribute('data-page-is-not-creatable')),
       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')),
       tags: null,
       hasChildren: JSON.parse(mainContent.getAttribute('data-page-has-children')),
@@ -143,16 +146,11 @@ export default class PageContainer extends Container {
   }
 
 
-  get isEditable() {
+  get isAbleToOpenPageEditor() {
     const { currentUser } = this.appContainer;
-    const {
-      isPageExist, isPageForbidden, isNotCreatable, isTrashPage,
-    } = this.state;
+    const { isPageForbidden, isNotCreatable, isTrashPage } = this.state;
 
-    if (isPageExist && (currentUser != null) && !isPageForbidden && !isNotCreatable && !isTrashPage) {
-      return true;
-    }
-    return false;
+    return ((currentUser != null) && !isPageForbidden && !isNotCreatable && !isTrashPage);
   }
 
   /**

+ 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 %}