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

Merge branch 'master' into feat/3176-grid-edit-modal-for-master-merge

itizawa 5 лет назад
Родитель
Сommit
569b8bc63e
62 измененных файлов с 916 добавлено и 540 удалено
  1. 18 11
      .github/workflows/ci.yml
  2. 5 0
      resource/locales/en_US/translation.json
  3. 5 0
      resource/locales/ja_JP/translation.json
  4. 7 1
      resource/locales/zh_CN/translation.json
  5. 18 11
      src/client/js/app.jsx
  6. 1 0
      src/client/js/components/Admin/App/GcsSettings.jsx
  7. 113 39
      src/client/js/components/CustomNavigation.jsx
  8. 28 0
      src/client/js/components/Icons/BookmarkIcon.jsx
  9. 0 0
      src/client/js/components/Icons/HistoryIcon.jsx
  10. 44 0
      src/client/js/components/Icons/RecentlyCreatedIcon.jsx
  11. 0 109
      src/client/js/components/MyBookmarkList/MyBookmarkList.jsx
  12. 7 6
      src/client/js/components/MyDraftList/Draft.jsx
  13. 9 7
      src/client/js/components/MyDraftList/MyDraftList.jsx
  14. 18 28
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  15. 4 4
      src/client/js/components/NotFoundPage.jsx
  16. 7 1
      src/client/js/components/Page/NotFoundAlert.jsx
  17. 31 5
      src/client/js/components/Page/PageManagement.jsx
  18. 5 5
      src/client/js/components/Page/TagEditModal.jsx
  19. 19 11
      src/client/js/components/Page/TagLabels.jsx
  20. 57 99
      src/client/js/components/PageAccessoriesModal.jsx
  21. 17 16
      src/client/js/components/PageComment/Comment.jsx
  22. 1 1
      src/client/js/components/PageComments.jsx
  23. 3 3
      src/client/js/components/PageList.jsx
  24. 97 0
      src/client/js/components/PageList/BookmarkList.jsx
  25. 3 9
      src/client/js/components/PaginationWrapper.jsx
  26. 6 7
      src/client/js/components/RecentCreated/RecentCreated.jsx
  27. 1 1
      src/client/js/components/Sidebar/SidebarNav.jsx
  28. 41 6
      src/client/js/components/TableOfContents.jsx
  29. 27 17
      src/client/js/components/TopOfTableContents.jsx
  30. 2 2
      src/client/js/components/TrashPageList.jsx
  31. 41 0
      src/client/js/components/User/UserInfo.jsx
  32. 0 5
      src/client/js/legacy/crowi.js
  33. 9 0
      src/client/js/services/NavigationContainer.js
  34. 2 2
      src/client/js/services/PageContainer.js
  35. 5 0
      src/client/styles/scss/_comment.scss
  36. 3 8
      src/client/styles/scss/_comment_growi.scss
  37. 3 9
      src/client/styles/scss/_draft.scss
  38. 1 1
      src/client/styles/scss/_layout_growi.scss
  39. 11 1
      src/client/styles/scss/_page_list.scss
  40. 0 4
      src/client/styles/scss/_subnav.scss
  41. 14 2
      src/client/styles/scss/_toc.scss
  42. 2 2
      src/client/styles/scss/_user.scss
  43. 23 5
      src/client/styles/scss/theme/_apply-colors-dark.scss
  44. 23 0
      src/client/styles/scss/theme/_apply-colors-light.scss
  45. 43 7
      src/client/styles/scss/theme/_apply-colors.scss
  46. 62 0
      src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss
  47. 1 2
      src/server/routes/apiv3/app-settings.js
  48. 4 3
      src/server/routes/apiv3/page.js
  49. 2 0
      src/server/routes/index.js
  50. 5 0
      src/server/routes/me.js
  51. 0 1
      src/server/views/layout-growi/forbidden.html
  52. 6 6
      src/server/views/layout-growi/page.html
  53. 3 3
      src/server/views/layout-growi/page_list.html
  54. 3 3
      src/server/views/layout-growi/shared_page.html
  55. 30 6
      src/server/views/layout-growi/user_page.html
  56. 2 2
      src/server/views/layout-growi/widget/comments.html
  57. 18 0
      src/server/views/me/drafts.html
  58. 1 1
      src/server/views/widget/alert_siteurl_undefined.html
  59. 2 0
      src/server/views/widget/forbidden_content.html
  60. 1 1
      src/server/views/widget/not_creatable_content.html
  61. 2 2
      src/server/views/widget/page_content.html
  62. 0 65
      src/server/views/widget/user_page_content.html

+ 18 - 11
.github/workflows/ci.yml

@@ -70,6 +70,12 @@ jobs:
       matrix:
       matrix:
         node-version: [14.x]
         node-version: [14.x]
 
 
+    services:
+      mongodb:
+        image: mongo:4.4
+        ports:
+        - 27017/tcp
+
     steps:
     steps:
     - uses: actions/checkout@v2
     - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
     - name: Use Node.js ${{ matrix.node-version }}
@@ -103,15 +109,11 @@ jobs:
         echo -n "node " && node -v
         echo -n "node " && node -v
         echo -n "npm " && npm -v
         echo -n "npm " && npm -v
         yarn list --depth=0
         yarn list --depth=0
-    - name: Launch MongoDB
-      uses: wbari/start-mongoDB@v0.2
-      with:
-        mongoDBVersion: 3.6
     - name: yarn test
     - name: yarn test
       run: |
       run: |
         yarn test
         yarn test
       env:
       env:
-        MONGO_URI: mongodb://localhost:27017/growi_test
+        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi_test
 
 
     - name: Slack Notification
     - name: Slack Notification
       uses: weseek/ghaction-slack-notification@master
       uses: weseek/ghaction-slack-notification@master
@@ -202,6 +204,12 @@ jobs:
       matrix:
       matrix:
         node-version: [12.x, 14.x]
         node-version: [12.x, 14.x]
 
 
+    services:
+      mongodb:
+        image: mongo:4.4
+        ports:
+        - 27017/tcp
+
     steps:
     steps:
     - uses: actions/checkout@v2
     - uses: actions/checkout@v2
     - name: Use Node.js ${{ matrix.node-version }}
     - name: Use Node.js ${{ matrix.node-version }}
@@ -254,16 +262,15 @@ jobs:
         echo -n "node " && node -v
         echo -n "node " && node -v
         echo -n "npm " && npm -v
         echo -n "npm " && npm -v
         yarn list --production --depth=0
         yarn list --production --depth=0
-    - name: Launch MongoDB
-      uses: wbari/start-mongoDB@v0.2
-      with:
-        mongoDBVersion: 3.6
+    - name: Get DB name
+      id: getdbname
+      run: |
+        echo ::set-output name=suffix::$(echo '${{ matrix.node-version }}' | sed s/\\.//)
     - name: yarn server:prod:ci
     - name: yarn server:prod:ci
       run: |
       run: |
         yarn server:prod:ci
         yarn server:prod:ci
       env:
       env:
-        MONGO_URI: mongodb://localhost:27017/growi
-
+        MONGO_URI: mongodb://localhost:${{ job.services.mongodb.ports['27017'] }}/growi-${{ steps.getdbname.outputs.suffix }}
     - name: Upload report as artifact
     - name: Upload report as artifact
       uses: actions/upload-artifact@v2
       uses: actions/upload-artifact@v2
       with:
       with:

+ 5 - 0
resource/locales/en_US/translation.json

@@ -1,5 +1,6 @@
 {
 {
   "Help": "Help",
   "Help": "Help",
+  "view": "View",
   "Edit": "Edit",
   "Edit": "Edit",
   "Delete": "Delete",
   "Delete": "Delete",
   "delete_all": "Delete all",
   "delete_all": "Delete all",
@@ -304,6 +305,9 @@
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
     }
     }
   },
   },
+  "page_comment": {
+    "display_the_page_when_posting_this_comment": "Display the page when posting this comment"
+  },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "already_exists": "New page is already exists.",
     "already_exists": "New page is already exists.",
@@ -443,6 +447,7 @@
     "open_sandbox": "Open Sandbox"
     "open_sandbox": "Open Sandbox"
   },
   },
   "hackmd": {
   "hackmd": {
+    "hack_md": "HackMD",
     "not_set_up": "HackMD is not set up.",
     "not_set_up": "HackMD is not set up.",
     "used_for_not_found": "Can not use HackMD to a page that does not exist.",
     "used_for_not_found": "Can not use HackMD to a page that does not exist.",
     "start_to_edit": "Start to edit with HackMD",
     "start_to_edit": "Start to edit with HackMD",

+ 5 - 0
resource/locales/ja_JP/translation.json

@@ -1,5 +1,6 @@
 {
 {
   "Help": "ヘルプ",
   "Help": "ヘルプ",
+  "view": "View",
   "Edit": "編集",
   "Edit": "編集",
   "Delete": "削除",
   "Delete": "削除",
   "delete_all": "全て削除",
   "delete_all": "全て削除",
@@ -306,6 +307,9 @@
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
     }
     }
   },
   },
+  "page_comment": {
+    "display_the_page_when_posting_this_comment": "投稿時のページを表示する"
+  },
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "already_exists": "新しいページが既に存在しています。",
     "already_exists": "新しいページが既に存在しています。",
@@ -445,6 +449,7 @@
     "open_sandbox": "Sandbox を開く"
     "open_sandbox": "Sandbox を開く"
   },
   },
   "hackmd":{
   "hackmd":{
+    "hack_md": "HackMD",
     "not_set_up": "HackMD はセットアップされていません",
     "not_set_up": "HackMD はセットアップされていません",
     "used_for_not_found": "HackMD は新しいページの作成には利用できません",
     "used_for_not_found": "HackMD は新しいページの作成には利用できません",
     "start_to_edit": "HackMD を開始する",
     "start_to_edit": "HackMD を開始する",

+ 7 - 1
resource/locales/zh_CN/translation.json

@@ -1,5 +1,6 @@
 {
 {
-	"Help": "帮助",
+  "Help": "帮助",
+  "view": "View",
 	"Edit": "编辑",
 	"Edit": "编辑",
 	"Delete": "删除",
 	"Delete": "删除",
 	"delete_all": "删除所有",
 	"delete_all": "删除所有",
@@ -80,6 +81,7 @@
 	"Shrink versions that have no diffs": "收缩没有差异的版本",
 	"Shrink versions that have no diffs": "收缩没有差异的版本",
 	"User ID": "用户ID",
 	"User ID": "用户ID",
 	"Home": "首页",
 	"Home": "首页",
+	"My Drafts": "My Drafts",
 	"User Settings": "用户设置",
 	"User Settings": "用户设置",
 	"User Information": "用户信息",
 	"User Information": "用户信息",
 	"Basic Info": "基础信息",
 	"Basic Info": "基础信息",
@@ -284,6 +286,9 @@
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
 			"conflict": "无法保存您所做的更改,因为其他人正在编辑此页。请在重新加载页面后重新编辑受影响的部分。"
 		}
 		}
 	},
 	},
+  "page_comment": {
+    "display_the_page_when_posting_this_comment": "Display the page when posting this comment"
+  },
 	"page_api_error": {
 	"page_api_error": {
 		"notfound_or_forbidden": "未找到或禁止原始页。",
 		"notfound_or_forbidden": "未找到或禁止原始页。",
 		"already_exists": "新建页面已存在",
 		"already_exists": "新建页面已存在",
@@ -419,6 +424,7 @@
 		"open_sandbox": "开放式沙箱"
 		"open_sandbox": "开放式沙箱"
 	},
 	},
 	"hackmd": {
 	"hackmd": {
+    "hack_md": "HackMD",
     "not_set_up": "HackMD is not set up.",
     "not_set_up": "HackMD is not set up.",
     "used_for_not_found": "Can not use HackMD to a page that does not exist.",
     "used_for_not_found": "Can not use HackMD to a page that does not exist.",
 		"start_to_edit": "Start to edit with HackMD",
 		"start_to_edit": "Start to edit with HackMD",

+ 18 - 11
src/client/js/app.jsx

@@ -21,10 +21,14 @@ import NotFoundPage from './components/NotFoundPage';
 import NotFoundAlert from './components/Page/NotFoundAlert';
 import NotFoundAlert from './components/Page/NotFoundAlert';
 import PageStatusAlert from './components/PageStatusAlert';
 import PageStatusAlert from './components/PageStatusAlert';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import RecentCreated from './components/RecentCreated/RecentCreated';
-import MyBookmarkList from './components/MyBookmarkList/MyBookmarkList';
+import RecentlyCreatedIcon from './components/Icons/RecentlyCreatedIcon';
+import MyDraftList from './components/MyDraftList/MyDraftList';
+import BookmarkIcon from './components/Icons/BookmarkIcon';
+import BookmarkList from './components/PageList/BookmarkList';
 import SeenUserList from './components/User/SeenUserList';
 import SeenUserList from './components/User/SeenUserList';
 import LikerList from './components/User/LikerList';
 import LikerList from './components/User/LikerList';
 import TableOfContents from './components/TableOfContents';
 import TableOfContents from './components/TableOfContents';
+import UserInfo from './components/User/UserInfo';
 import Fab from './components/Fab';
 import Fab from './components/Fab';
 
 
 import PersonalSettings from './components/Me/PersonalSettings';
 import PersonalSettings from './components/Me/PersonalSettings';
@@ -80,12 +84,18 @@ Object.assign(componentMappings, {
 
 
   'not-found-page': <NotFoundPage />,
   'not-found-page': <NotFoundPage />,
 
 
-  'not-found-alert': <NotFoundAlert onPageCreateClicked={navigationContainer.setEditorMode} />,
+  'not-found-alert': <NotFoundAlert
+    onPageCreateClicked={navigationContainer.setEditorMode}
+    isForbidden={pageContainer.state.isForbidden}
+    isNotCreatable={pageContainer.state.isNotCreatable}
+  />,
 
 
   'page-timeline': <PageTimeline />,
   'page-timeline': <PageTimeline />,
 
 
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
 
 
+  'my-drafts': <MyDraftList />,
+
   'grw-fab-container': <Fab />,
   'grw-fab-container': <Fab />,
 });
 });
 
 
@@ -95,13 +105,14 @@ if (pageContainer.state.pageId != null) {
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-management': <PageManagement />,
     'page-management': <PageManagement />,
-    'revision-toc': <TableOfContents />,
+    'revision-toc': <TableOfContents isGuestUserMode={appContainer.currentUser == null} />,
     'seen-user-list': <SeenUserList />,
     'seen-user-list': <SeenUserList />,
     'liker-list': <LikerList />,
     'liker-list': <LikerList />,
 
 
-    'user-bookmark-list': <MyBookmarkList />,
-    'user-created-list': <RecentCreated />,
-    // 'user-draft-list': <MyDraftList />,
+    'recent-created-icon': <RecentlyCreatedIcon />,
+    'user-created-list': <RecentCreated userId={pageContainer.state.creator._id} />,
+    'user-bookmark-icon': <BookmarkIcon />,
+    'user-bookmark-list': <BookmarkList userId={pageContainer.state.creator._id} />,
   });
   });
 }
 }
 if (pageContainer.state.creator != null) {
 if (pageContainer.state.creator != null) {
@@ -115,11 +126,7 @@ if (pageContainer.state.path != null) {
     'page': <Page />,
     'page': <Page />,
     'grw-subnav-container': <GrowiSubNavigation />,
     'grw-subnav-container': <GrowiSubNavigation />,
     'grw-subnav-switcher-container': <GrowiSubNavigationSwitcher />,
     'grw-subnav-switcher-container': <GrowiSubNavigationSwitcher />,
-  });
-}
-// additional definitions if user is logged in
-if (appContainer.currentUser != null) {
-  Object.assign(componentMappings, {
+    'user-info': <UserInfo pageUser={pageContainer.state.pageUser} />,
     'display-switcher': <DisplaySwitcher />,
     'display-switcher': <DisplaySwitcher />,
   });
   });
 }
 }

+ 1 - 0
src/client/js/components/Admin/App/GcsSettings.jsx

@@ -18,6 +18,7 @@ function GcsSetting(props) {
       {gcsUseOnlyEnvVars && (
       {gcsUseOnlyEnvVars && (
         <p
         <p
           className="alert alert-info"
           className="alert alert-info"
+          // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{ __html: t('admin:app_setting.note_for_the_only_env_option', { env: 'IS_GCS_ENV_PRIORITIZED' }) }}
           dangerouslySetInnerHTML={{ __html: t('admin:app_setting.note_for_the_only_env_option', { env: 'IS_GCS_ENV_PRIORITIZED' }) }}
         />
         />
       )}
       )}

+ 113 - 39
src/client/js/components/CustomNavigation.jsx

@@ -1,18 +1,37 @@
-import React, { useEffect, useState } from 'react';
+import React, {
+  useEffect, useState, useRef, useMemo, useCallback,
+} from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import {
 import {
   Nav, NavItem, NavLink, TabContent, TabPane,
   Nav, NavItem, NavLink, TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 
 
-const CustomNavigation = (props) => {
-  const [activeTab, setActiveTab] = useState('');
-  // [TODO: set default active tab by gw4079]
-  const [sliderWidth, setSliderWidth] = useState(null);
-  const [sliderMarginLeft, setSliderMarginLeft] = useState(null);
+export const CustomNav = (props) => {
+  const navContainer = useRef();
+  const [sliderWidth, setSliderWidth] = useState(0);
+  const [sliderMarginLeft, setSliderMarginLeft] = useState(0);
+
+  const { activeTab, navTabMapping, onNavSelected } = props;
+
+  const navTabRefs = useMemo(() => {
+    const obj = {};
+    Object.keys(navTabMapping).forEach((key) => {
+      obj[key] = React.createRef();
+    });
+    return obj;
+  }, [navTabMapping]);
+
+  const navLinkClickHandler = useCallback((key) => {
+    if (onNavSelected != null) {
+      onNavSelected(key);
+    }
+  }, [onNavSelected]);
 
 
-  function switchActiveTab(activeTab) {
-    setActiveTab(activeTab);
+  function registerNavLink(key, elm) {
+    if (elm != null) {
+      navTabRefs[key] = elm;
+    }
   }
   }
 
 
   // Might make this dynamic for px, %, pt, em
   // Might make this dynamic for px, %, pt, em
@@ -25,59 +44,114 @@ const CustomNavigation = (props) => {
       return;
       return;
     }
     }
 
 
-    const navBar = document.getElementById('grw-custom-navbar');
-    const navTabs = document.querySelectorAll('ul.grw-custom-navbar > li.grw-custom-navtab');
-
-    if (navBar == null || navTabs == null) {
+    if (navContainer == null) {
       return;
       return;
     }
     }
 
 
     let tempML = 0;
     let tempML = 0;
 
 
-    const styles = [].map.call(navTabs, (el) => {
-      const width = getPercentage(el.offsetWidth, navBar.offsetWidth);
+    const styles = Object.entries(navTabRefs).map((el) => {
+      const width = getPercentage(el[1].offsetWidth, navContainer.current.offsetWidth);
       const marginLeft = tempML;
       const marginLeft = tempML;
       tempML += width;
       tempML += width;
       return { width, marginLeft };
       return { width, marginLeft };
     });
     });
-    const { width, marginLeft } = styles[props.navTabMapping[activeTab].index];
+    const { width, marginLeft } = styles[navTabMapping[activeTab].index];
 
 
     setSliderWidth(width);
     setSliderWidth(width);
     setSliderMarginLeft(marginLeft);
     setSliderMarginLeft(marginLeft);
 
 
-  }, [activeTab]);
+  }, [activeTab, navTabRefs, navTabMapping]);
+
+  return (
+    <>
+      <div ref={navContainer}>
+        <Nav className="nav-title grw-custom-navbar" id="grw-custom-navbar">
+          {Object.entries(navTabMapping).map(([key, value]) => {
+
+            const isActive = activeTab === key;
+            const isLinkEnabled = value.isLinkEnabled != null ? value.isLinkEnabled(value) : true;
+            const { Icon, i18n } = value;
+
+            return (
+              <NavItem
+                key={key}
+                type="button"
+                className={`p-0 grw-custom-navtab ${isActive && 'active'}}`}
+              >
+                <NavLink key={key} innerRef={elm => registerNavLink(key, elm)} disabled={!isLinkEnabled} onClick={() => navLinkClickHandler(key)}>
+                  <Icon /> {i18n}
+                </NavLink>
+              </NavItem>
+            );
+          })}
+        </Nav>
+      </div>
+      <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
+    </>
+  );
+
+};
+
+CustomNav.propTypes = {
+  activeTab: PropTypes.string.isRequired,
+  navTabMapping: PropTypes.object.isRequired,
+  onNavSelected: PropTypes.func,
+};
+
 
 
+export const CustomTabContent = (props) => {
+
+  const { activeTab, navTabMapping, additionalClassNames } = props;
+
+  return (
+    <TabContent activeTab={activeTab} className={additionalClassNames.join(' ')}>
+      {Object.entries(navTabMapping).map(([key, value]) => {
+
+        const { Content } = value;
+
+        return (
+          <TabPane key={key} tabId={key}>
+            <Content />
+          </TabPane>
+        );
+      })}
+    </TabContent>
+  );
+
+};
+
+CustomTabContent.propTypes = {
+  activeTab: PropTypes.string.isRequired,
+  navTabMapping: PropTypes.object.isRequired,
+  additionalClassNames: PropTypes.arrayOf(PropTypes.string),
+};
+CustomTabContent.defaultProps = {
+  additionalClassNames: [],
+};
+
+
+const CustomNavigation = (props) => {
+  const { navTabMapping, defaultTabIndex, tabContentClasses } = props;
+  const [activeTab, setActiveTab] = useState(Object.keys(props.navTabMapping)[defaultTabIndex || 0]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
-      <Nav className="nav-title grw-custom-navbar" id="grw-custom-navbar">
-        {Object.entries(props.navTabMapping).map(([key, value]) => {
-          return (
-            <NavItem key={key} type="button" className={`p-0 grw-custom-navtab ${activeTab === key && 'active'}`}>
-              <NavLink onClick={() => { switchActiveTab(key) }}>
-                {value.icon}
-                {value.i18n}
-              </NavLink>
-            </NavItem>
-          );
-        })}
-      </Nav>
-      <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
-      <TabContent activeTab={activeTab} className="p-4">
-        {Object.entries(props.navTabMapping).map(([key, value]) => {
-          return (
-            <TabPane key={key} tabId={key}>
-              {value.tabContent}
-            </TabPane>
-          );
-        })}
-      </TabContent>
+
+      <CustomNav activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={setActiveTab} />
+      <CustomTabContent activeTab={activeTab} navTabMapping={navTabMapping} additionalClassNames={tabContentClasses} />
+
     </React.Fragment>
     </React.Fragment>
   );
   );
 };
 };
 
 
 CustomNavigation.propTypes = {
 CustomNavigation.propTypes = {
-  navTabMapping: PropTypes.object,
+  navTabMapping: PropTypes.object.isRequired,
+  defaultTabIndex: PropTypes.number,
+  tabContentClasses: PropTypes.arrayOf(PropTypes.string),
+};
+CustomNavigation.defaultProps = {
+  tabContentClasses: ['p-4'],
 };
 };
 
 
 export default CustomNavigation;
 export default CustomNavigation;

+ 28 - 0
src/client/js/components/Icons/BookmarkIcon.jsx

@@ -0,0 +1,28 @@
+import React from 'react';
+
+const BookmarkIcon = () => (
+
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="20"
+    height="20"
+    viewBox="0 0 20 20"
+  >
+
+    <g transform="translate(-925.888 168.873)">
+      <rect width="20" height="20" transform="translate(925.888 -168.873)" fill="none" />
+      <path d="M936.092-168.527a1.141,1.141,0,0,1,.205.039,1.685,1.685,0,0,1,.185.068c.058.026.116.056.175.088a1.038,1.038,0,0,1,
+        .166.117,1.826,1.826,0,0,1,.146.146c.045.052.088.1.127.156a.8.8,0,0,1,.1.175l2.26,4.7,5.2.76a1.424,1.424,0,0,1,.7.311,1.413,
+        1.413,0,0,1,.449.643,1.294,1.294,0,0,1-.351,1.423l-3.8,3.8.876,5.28a1.225,1.225,0,0,1-.088.76,1.451,1.451,0,0,1-.5.6,1.456,
+        1.456,0,0,1-.838.253,1.614,1.614,0,0,1-.351-.039,1.316,1.316,0,0,1-.35-.137l-4.52-2.435-4.54,2.435a1.37,1.37,0,0,1-.682.176h-.156a.525.525,
+        0,0,1-.146-.02l-.137-.039a1.117,1.117,0,0,1-.136-.049,1.231,1.231,0,0,1-.136-.068c-.046-.026-.088-.052-.127-.077a1.462,1.462,
+        0,0,1-.5-.6,1.232,1.232,0,0,1-.087-.76l.877-5.28-3.8-3.8a1.29,1.29,0,0,1-.35-1.423,1.4,1.4,0,0,1,.448-.643,1.423,1.423,0,0,1,
+        .7-.311l5.2-.76,2.26-4.7a1.351,1.351,0,0,1,.526-.584,1.467,1.467,0,0,1,.78-.215C935.953-168.537,936.02-168.533,936.092-168.527Zm-2.49,
+        5.9-.41.84-6.1.9,4.415,4.415-.136.879-.9,5.275,5.412-2.891,5.411,2.891-.9-5.275-.137-.879,4.415-4.415-6.115-.9-2.676-5.587Z"
+      />
+    </g>
+  </svg>
+
+);
+
+export default BookmarkIcon;

+ 0 - 0
src/client/js/components/Icons/RecentChangesIcon.jsx → src/client/js/components/Icons/HistoryIcon.jsx


+ 44 - 0
src/client/js/components/Icons/RecentlyCreatedIcon.jsx

@@ -0,0 +1,44 @@
+import React from 'react';
+
+const RecentlyCreatedIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="20"
+    height="20"
+    viewBox="0 0 20 20"
+  >
+
+    <g transform="translate(-921.906 192.966)">
+
+      <rect
+        width="20"
+        height="20"
+        transform="translate(921.906 -192.966)"
+        fill="none"
+      />
+      <path
+        d="M933.752-189.286l.022-.009a3.3,3.3,0,0,1,1.556.927,2.991,2.991,0,0,1,.505.679,3.659,3.659,0,0,1,
+        .265.572c.038.126.069.245.091.356l-.911.9a6.484,6.484,0,0,1,1.086-.1c.177,0,.35.013.523.027.573-.571.93-.928,1.043-1.047a2.94,
+        2.94,0,0,0,.959-2.086,2.854,2.854,0,0,0-1.008-1.986,3.3,3.3,0,0,0-.9-.629,2.344,2.344,0,0,0-.986-.215,
+        2.836,2.836,0,0,0-2.053.91q-.3.28-10.478,10.478a.656.656,0,0,0-.149.232q-.066.28-1.391,4.651a.529.529,0,0,0,
+        .149.546c.036.032.084.073.1.086a.937.937,0,0,0,.124.057.585.585,0,0,0,.3-.007q3.493-1.147,4.57-1.461a.549.549,0,0,0,.124-.048.517.517,
+        0,0,0,.108-.083q.958-.952,2.5-2.483a2.017,2.017,0,0,0,.035-.513,6.356,6.356,0,0,1,.107-1.143l-2.558,2.531a4.537,4.537,0,0,0-.91-1.357,
+        4.672,4.672,0,0,0-1.556-1.043Zm.975-.953.033-.032a2.254,2.254,0,0,1,.207-.183,2.379,2.379,0,0,1,.447-.248,1.51,1.51,0,0,1,.637-.149,
+        1.418,1.418,0,0,1,.587.133,1.937,1.937,0,0,1,.555.4,2.714,2.714,0,0,1,.5.629,1.266,1.266,0,0,1,.173.612,1.926,1.926,0,0,1-.661,1.289.052.052,
+        0,0,1-.016.033l-.033.032-.048.049a4.42,4.42,0,0,0-.96-1.507,4.709,4.709,0,0,0-1.473-1.011Zm-9.692,13.375-1.794.6q.148-.5.546-1.73t.511-1.648a3.4,
+        3.4,0,0,1,1.521.926,3.151,3.151,0,0,1,.8,1.324q-.333.118-1.582.53Z"
+      />
+      <path
+        d="M938.7-176.431a.5.5,0,0,1-.359-.151l-2.276-2.355a.5.5,0,0,1-.14-.347v-3.425a.5.5,0,0,1,.5-.5h0a.5.5,0,0,1,.5.5h0v3.225l2.135
+        ,2.209a.5.5,0,0,1-.011.7h0A.49.49,0,0,1,938.7-176.431Z"
+      />
+      <path
+        d="M936.422-185.009a5.49,5.49,0,0,0-5.484,5.484,5.487,5.487,0,0,0,5.484,5.484,5.491,5.491,0,0,0,5.484-5.484A5.491,5.491,0,0,0,
+        936.422-185.009Zm0,9.97a4.487,4.487,0,0,1-4.487-4.487,4.486,4.486,0,0,1,4.487-4.486,4.486,4.486,0,0,1,4.487,4.486A4.487,
+        4.487,0,0,1,936.422-175.039Z"
+      />
+    </g>
+  </svg>
+);
+
+export default RecentlyCreatedIcon;

+ 0 - 109
src/client/js/components/MyBookmarkList/MyBookmarkList.jsx

@@ -1,109 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import loggerFactory from '@alias/logger';
-import { withUnstatedContainers } from '../UnstatedUtils';
-
-
-import AppContainer from '../../services/AppContainer';
-import PageContainer from '../../services/PageContainer';
-import { toastError } from '../../util/apiNotification';
-
-import PaginationWrapper from '../PaginationWrapper';
-
-import Page from '../PageList/Page';
-
-const logger = loggerFactory('growi:MyBookmarkList');
-class MyBookmarkList extends React.Component {
-
-  constructor(props) {
-    super(props);
-
-    this.state = {
-      pages: [],
-      activePage: 1,
-      totalPages: 0,
-      pagingLimit: null,
-    };
-
-    this.handlePage = this.handlePage.bind(this);
-  }
-
-  componentWillMount() {
-    this.getMyBookmarkList(1);
-  }
-
-  async handlePage(selectPageNumber) {
-    await this.getMyBookmarkList(selectPageNumber);
-  }
-
-  async getMyBookmarkList(selectPageNumber) {
-    const { appContainer } = this.props;
-    const userId = appContainer.currentUserId;
-    const page = selectPageNumber;
-
-    try {
-      const { data } = await this.props.appContainer.apiv3.get(`/bookmarks/${userId}`, { page });
-      if (data.paginationResult == null) {
-        throw new Error('data must conclude \'paginateResult\' property.');
-      }
-      const {
-        docs: pages, totalDocs: totalPages, limit: pagingLimit, page: activePage,
-      } = data.paginationResult;
-      this.setState({
-        pages,
-        totalPages,
-        pagingLimit,
-        activePage,
-      });
-    }
-    catch (error) {
-      logger.error('failed to fetch data', error);
-      toastError(error, 'Error occurred in bookmark page list');
-    }
-  }
-
-  /**
-   * generate Elements of Page
-   *
-   * @param {any} pages Array of pages Model Obj
-   *
-   */
-  generatePageList(pages) {
-    return pages.map(page => (
-      <li key={`my-bookmarks:${page._id}`}>
-        <Page page={page.page} />
-      </li>
-    ));
-  }
-
-
-  render() {
-    return (
-      <div className="page-list-container-create">
-        <ul className="page-list-ul page-list-ul-flat mb-3">
-          {this.generatePageList(this.state.pages)}
-        </ul>
-        <PaginationWrapper
-          activePage={this.state.activePage}
-          changePage={this.handlePage}
-          totalItemsCount={this.state.totalPages}
-          pagingLimit={this.state.pagingLimit}
-        />
-      </div>
-    );
-  }
-
-}
-
-/**
- * Wrapper component for using unstated
- */
-const MyBookmarkListWrapper = withUnstatedContainers(MyBookmarkList, [AppContainer, PageContainer]);
-
-MyBookmarkList.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
-};
-
-export default MyBookmarkListWrapper;

+ 7 - 6
src/client/js/components/MyDraftList/Draft.jsx

@@ -82,20 +82,21 @@ class Draft extends React.Component {
 
 
   renderAccordionTitle(isExist) {
   renderAccordionTitle(isExist) {
     const { isPanelExpanded } = this.state;
     const { isPanelExpanded } = this.state;
-
-    const iconClass = isPanelExpanded ? 'caret-opened' : '';
+    const { t } = this.props;
+    const iconClass = isPanelExpanded ? 'fa-rotate-90' : '';
 
 
     return (
     return (
       <span>
       <span>
-        <i className={`caret ${iconClass}`}></i>
-        <span className="mx-2" onClick={() => this.setState({ isPanelExpanded: !isPanelExpanded })}>
+
+        <span className="mr-2 draft-path" onClick={() => this.setState({ isPanelExpanded: !isPanelExpanded })}>
+          <i className={`fa fa-fw fa-angle-right mr-2 ${iconClass}`}></i>
           {this.props.path}
           {this.props.path}
         </span>
         </span>
         { isExist && (
         { isExist && (
-          <span>({this.props.t('page exists')})</span>
+          <span className="badge badge-warning">{t('page exists')}</span>
         ) }
         ) }
         { !isExist && (
         { !isExist && (
-          <span className="badge badge-secondary">draft</span>
+          <span className="badge badge-info">draft</span>
         ) }
         ) }
 
 
         <a className="ml-2" href={this.props.path}><i className="icon icon-login"></i></a>
         <a className="ml-2" href={this.props.path}><i className="icon icon-login"></i></a>

+ 9 - 7
src/client/js/components/MyDraftList/MyDraftList.jsx

@@ -68,8 +68,8 @@ class MyDraftList extends React.Component {
   }
   }
 
 
   getCurrentDrafts(selectPageNumber) {
   getCurrentDrafts(selectPageNumber) {
-    // TODO implement temporarily paging number only this component (this paging size is pageLimitationL).
-    const limit = this.state.pagingLimit;
+
+    const limit = 50; // implement only this component.(this default value is 50 (pageLimitationL))
 
 
     const totalDrafts = this.state.drafts.length;
     const totalDrafts = this.state.drafts.length;
     const activePage = selectPageNumber;
     const activePage = selectPageNumber;
@@ -134,15 +134,16 @@ class MyDraftList extends React.Component {
     const totalCount = this.state.totalDrafts;
     const totalCount = this.state.totalDrafts;
 
 
     return (
     return (
-      <div>
-
+      <div className="page-list-container-create ">
+        <h1>My Drafts</h1>
+        <hr />
         { totalCount === 0
         { totalCount === 0
-          && <span>No drafts yet.</span>
+          && <span className="mt-2">No drafts yet.</span>
         }
         }
 
 
         { totalCount > 0 && (
         { totalCount > 0 && (
           <React.Fragment>
           <React.Fragment>
-            <div className="d-flex justify-content-between">
+            <div className="d-flex justify-content-between mt-2">
               <h4>Total: {totalCount} drafts</h4>
               <h4>Total: {totalCount} drafts</h4>
               <div className="align-self-center">
               <div className="align-self-center">
                 <button type="button" className="btn btn-sm btn-outline-danger" onClick={this.clearAllDrafts}>
                 <button type="button" className="btn btn-sm btn-outline-danger" onClick={this.clearAllDrafts}>
@@ -152,7 +153,7 @@ class MyDraftList extends React.Component {
               </div>
               </div>
             </div>
             </div>
 
 
-            <div className="tab-pane mt-5 accordion" id="draft-list">
+            <div className="tab-pane mt-2 accordion" id="draft-list">
               {draftList}
               {draftList}
             </div>
             </div>
             <PaginationWrapper
             <PaginationWrapper
@@ -160,6 +161,7 @@ class MyDraftList extends React.Component {
               changePage={this.handlePage}
               changePage={this.handlePage}
               totalItemsCount={this.state.totalDrafts}
               totalItemsCount={this.state.totalDrafts}
               pagingLimit={this.state.pagingLimit}
               pagingLimit={this.state.pagingLimit}
+              align="center"
               size="sm"
               size="sm"
             />
             />
           </React.Fragment>
           </React.Fragment>

+ 18 - 28
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -22,7 +22,6 @@ import ThreeStrandedButton from './ThreeStrandedButton';
 
 
 import AuthorInfo from './AuthorInfo';
 import AuthorInfo from './AuthorInfo';
 import DrawerToggler from './DrawerToggler';
 import DrawerToggler from './DrawerToggler';
-import UserPicture from '../User/UserPicture';
 
 
 import PageManagement from '../Page/PageManagement';
 import PageManagement from '../Page/PageManagement';
 
 
@@ -65,7 +64,8 @@ const PagePathNav = ({ pageId, pagePath, isPageForbidden }) => {
   );
   );
 };
 };
 
 
-// eslint-disable-next-line react/prop-types
+/* eslint-disable react/prop-types */
+// eslint-disable-next-line no-unused-vars
 const UserPagePathNav = ({ pageId, pagePath }) => {
 const UserPagePathNav = ({ pageId, pagePath }) => {
   const linkedPagePath = new LinkedPagePath(pagePath);
   const linkedPagePath = new LinkedPagePath(pagePath);
   const latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />;
   const latterLink = <PagePathHierarchicalLink linkedPagePath={linkedPagePath} />;
@@ -85,10 +85,11 @@ const UserPagePathNav = ({ pageId, pagePath }) => {
   );
   );
 };
 };
 
 
-/* eslint-disable react/prop-types */
+// eslint-disable-next-line no-unused-vars
 const UserInfo = ({ pageUser }) => {
 const UserInfo = ({ pageUser }) => {
   return (
   return (
     <div className="grw-users-info d-flex align-items-center">
     <div className="grw-users-info d-flex align-items-center">
+      {/* eslint-disable-next-line react/jsx-no-undef */}
       <UserPicture user={pageUser} />
       <UserPicture user={pageUser} />
 
 
       <div className="users-meta">
       <div className="users-meta">
@@ -144,6 +145,8 @@ const GrowiSubNavigation = (props) => {
 
 
   const { currentUser } = appContainer;
   const { currentUser } = appContainer;
   const isPageNotFound = pageId == null;
   const isPageNotFound = pageId == null;
+  // Tags cannot be edited while the new page and editorMode is view
+  const isTagLabelHidden = (editorMode !== 'edit' && isPageNotFound);
   const isUserPage = pageUser != null;
   const isUserPage = pageUser != null;
   const isPageInTrash = isTrashPage(path);
   const isPageInTrash = isTrashPage(path);
 
 
@@ -163,24 +166,12 @@ const GrowiSubNavigation = (props) => {
         ) }
         ) }
 
 
         <div className="grw-path-nav-container">
         <div className="grw-path-nav-container">
-          { !isCompactMode && !isPageNotFound && !isPageForbidden && !isUserPage && (
+          { !isCompactMode && !isTagLabelHidden && !isPageForbidden && !isUserPage && (
             <div className="mb-2">
             <div className="mb-2">
-              <TagLabels />
+              <TagLabels editorMode={editorMode} />
             </div>
             </div>
           ) }
           ) }
-
-          { isUserPage
-            ? (
-              <>
-                <UserPagePathNav pageId={pageId} pagePath={path} />
-                <UserInfo pageUser={pageUser} />
-              </>
-            )
-            : (
-              <PagePathNav pageId={pageId} pagePath={path} isPageForbidden={isPageForbidden} />
-            )
-          }
-
+          <PagePathNav pageId={pageId} pagePath={path} isPageForbidden={isPageForbidden} />
         </div>
         </div>
       </div>
       </div>
 
 
@@ -190,23 +181,22 @@ 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} /> }
             { !isPageInTrash && !isPageNotFound && !isPageForbidden && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
-            { !isPageNotFound && !isPageForbidden && <PageManagement /> }
+            { !isPageNotFound && !isPageForbidden && <PageManagement isCompactMode={isCompactMode} /> }
           </div>
           </div>
           <div className="mt-2">
           <div className="mt-2">
-            { !isCreatable && !isPageInTrash
-            && (
-            <ThreeStrandedButton
-              onThreeStrandedButtonClicked={onThreeStrandedButtonClicked}
-              isBtnDisabled={currentUser == null}
-              editorMode={editorMode}
-            />
-)}
+            { !isCreatable && !isPageInTrash && !isPageForbidden && (
+              <ThreeStrandedButton
+                onThreeStrandedButtonClicked={onThreeStrandedButtonClicked}
+                isBtnDisabled={currentUser == null}
+                editorMode={editorMode}
+              />
+            )}
           </div>
           </div>
         </div>
         </div>
 
 
         {/* Page Authors */}
         {/* Page Authors */}
         { (!isCompactMode && !isUserPage && !isPageNotFound && !isPageForbidden) && (
         { (!isCompactMode && !isUserPage && !isPageNotFound && !isPageForbidden) && (
-          <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none">
+          <ul className="authors text-nowrap border-left d-none d-lg-block d-edit-none py-2 pl-4 mb-0 ml-3">
             <li className="pb-1">
             <li className="pb-1">
               <AuthorInfo user={creator} date={createdAt} />
               <AuthorInfo user={creator} date={createdAt} />
             </li>
             </li>

+ 4 - 4
src/client/js/components/NotFoundPage.jsx

@@ -12,15 +12,15 @@ const NotFoundPage = (props) => {
 
 
   const navTabMapping = {
   const navTabMapping = {
     pagelist: {
     pagelist: {
-      icon: <PageListIcon />,
+      Icon: PageListIcon,
+      Content: PageList,
       i18n: t('page_list'),
       i18n: t('page_list'),
-      tabContent: <PageList />,
       index: 0,
       index: 0,
     },
     },
     timeLine: {
     timeLine: {
-      icon: <TimeLineIcon />,
+      Icon: TimeLineIcon,
+      Content: PageTimeline,
       i18n: t('Timeline View'),
       i18n: t('Timeline View'),
-      tabContent: <PageTimeline />,
       index: 1,
       index: 1,
     },
     },
   };
   };

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

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
 const NotFoundAlert = (props) => {
 const NotFoundAlert = (props) => {
-  const { t } = props;
+  const { t, isForbidden, isNotCreatable } = props;
   function clickHandler(viewType) {
   function clickHandler(viewType) {
     if (props.onPageCreateClicked === null) {
     if (props.onPageCreateClicked === null) {
       return;
       return;
@@ -11,6 +11,10 @@ const NotFoundAlert = (props) => {
     props.onPageCreateClicked(viewType);
     props.onPageCreateClicked(viewType);
   }
   }
 
 
+  if (isForbidden || isNotCreatable) {
+    return null;
+  }
+
   return (
   return (
     <div className="border border-info m-4 p-3">
     <div className="border border-info m-4 p-3">
       <div className="col-md-12 p-0">
       <div className="col-md-12 p-0">
@@ -35,6 +39,8 @@ const NotFoundAlert = (props) => {
 NotFoundAlert.propTypes = {
 NotFoundAlert.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   onPageCreateClicked: PropTypes.func,
   onPageCreateClicked: PropTypes.func,
+  isForbidden: PropTypes.bool.isRequired,
+  isNotCreatable: PropTypes.bool.isRequired,
 };
 };
 
 
 export default withTranslation()(NotFoundAlert);
 export default withTranslation()(NotFoundAlert);

+ 31 - 5
src/client/js/components/Page/PageManagement.jsx

@@ -17,7 +17,9 @@ import PresentationIcon from '../Icons/PresentationIcon';
 
 
 
 
 const PageManagement = (props) => {
 const PageManagement = (props) => {
-  const { t, appContainer, pageContainer } = props;
+  const {
+    t, appContainer, pageContainer, isCompactMode,
+  } = props;
   const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
   const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
 
 
   const { currentUser } = appContainer;
   const { currentUser } = appContainer;
@@ -100,6 +102,24 @@ const PageManagement = (props) => {
   //   setIsArchiveCreateModalShown(false);
   //   setIsArchiveCreateModalShown(false);
   // }
   // }
 
 
+  function renderDropdownItemForTopPage() {
+    return (
+      <>
+        <button className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
+          <i className="icon-fw icon-docs"></i> { t('Duplicate') }
+        </button>
+        {/* TODO Presentation Mode is not function. So if it is really necessary, survey this cause and implement Presentation Mode in top page */}
+        {/* <button className="dropdown-item" type="button" onClick={openPagePresentationModalHandler}>
+          <i className="icon-fw"><PresentationIcon /></i><span className="d-none d-sm-inline"> { t('Presentation Mode') }</span>
+        </button> */}
+        <button type="button" className="dropdown-item" onClick={() => { exportPageHandler('md') }}>
+          <i className="icon-fw icon-cloud-download"></i>{t('export_bulk.export_page_markdown')}
+        </button>
+        <div className="dropdown-divider"></div>
+      </>
+    );
+  }
+
   function renderDropdownItemForNotTopPage() {
   function renderDropdownItemForNotTopPage() {
     return (
     return (
       <>
       <>
@@ -110,7 +130,7 @@ const PageManagement = (props) => {
           <i className="icon-fw icon-docs"></i> { t('Duplicate') }
           <i className="icon-fw icon-docs"></i> { t('Duplicate') }
         </button>
         </button>
         <button className="dropdown-item" type="button" onClick={openPagePresentationModalHandler}>
         <button className="dropdown-item" type="button" onClick={openPagePresentationModalHandler}>
-          <i className="icon-fw"><PresentationIcon /></i><span className="d-none d-sm-inline"> { t('Presentation Mode') }</span>
+          <i className="icon-fw"><PresentationIcon /></i> { t('Presentation Mode') }
         </button>
         </button>
         <button type="button" className="dropdown-item" onClick={() => { exportPageHandler('md') }}>
         <button type="button" className="dropdown-item" onClick={() => { exportPageHandler('md') }}>
           <i className="icon-fw icon-cloud-download"></i>{t('export_bulk.export_page_markdown')}
           <i className="icon-fw icon-cloud-download"></i>{t('export_bulk.export_page_markdown')}
@@ -175,7 +195,7 @@ const PageManagement = (props) => {
       <>
       <>
         <button
         <button
           type="button"
           type="button"
-          className="btn-link nav-link dropdown-toggle dropdown-toggle-no-caret border-0 rounded grw-btn-page-management"
+          className={`btn-link nav-link dropdown-toggle dropdown-toggle-no-caret border-0 rounded grw-btn-page-management ${isCompactMode && 'py-0'}`}
           data-toggle="dropdown"
           data-toggle="dropdown"
         >
         >
           <i className="icon-options"></i>
           <i className="icon-options"></i>
@@ -189,7 +209,7 @@ const PageManagement = (props) => {
       <>
       <>
         <button
         <button
           type="button"
           type="button"
-          className="btn nav-link bg-transparent dropdown-toggle dropdown-toggle-no-caret disabled"
+          className={`btn nav-link bg-transparent dropdown-toggle dropdown-toggle-no-caret disabled ${isCompactMode && 'py-0'}`}
           id="icon-options-guest-tltips"
           id="icon-options-guest-tltips"
         >
         >
           <i className="icon-options"></i>
           <i className="icon-options"></i>
@@ -206,7 +226,7 @@ const PageManagement = (props) => {
     <>
     <>
       {currentUser == null ? renderDotsIconForGuestUser() : renderDotsIconForCurrentUser()}
       {currentUser == null ? renderDotsIconForGuestUser() : renderDotsIconForCurrentUser()}
       <div className="dropdown-menu dropdown-menu-right">
       <div className="dropdown-menu dropdown-menu-right">
-        {!isTopPagePath && renderDropdownItemForNotTopPage()}
+        {isTopPagePath ? renderDropdownItemForTopPage() : renderDropdownItemForNotTopPage()}
         <button className="dropdown-item" type="button" onClick={openPageTemplateModalHandler}>
         <button className="dropdown-item" type="button" onClick={openPageTemplateModalHandler}>
           <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
           <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
         </button>
         </button>
@@ -227,6 +247,12 @@ PageManagement.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isCompactMode: PropTypes.bool,
+};
+
+PageManagement.defaultProps = {
+  isCompactMode: false,
 };
 };
 
 
 export default withTranslation()(PageManagementWrapper);
 export default withTranslation()(PageManagementWrapper);

+ 5 - 5
src/client/js/components/Page/TagEditModal.jsx

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import {
 import {
-  Button, Modal, ModalHeader, ModalBody, ModalFooter,
+  Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import TagsInput from './TagsInput';
 import TagsInput from './TagsInput';
@@ -37,15 +37,15 @@ function TagEditModal(props) {
   return (
   return (
     <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal">
     <Modal isOpen={props.isOpen} toggle={closeModalHandler} id="edit-tag-modal">
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
       <ModalHeader tag="h4" toggle={closeModalHandler} className="bg-primary text-light">
-          Edit Tags
+        Edit Tags
       </ModalHeader>
       </ModalHeader>
       <ModalBody>
       <ModalBody>
         <TagsInput tags={tags} onTagsUpdated={onTagsUpdatedByTagsInput} />
         <TagsInput tags={tags} onTagsUpdated={onTagsUpdatedByTagsInput} />
       </ModalBody>
       </ModalBody>
       <ModalFooter>
       <ModalFooter>
-        <Button color="primary" onClick={handleSubmit}>
-            Done
-        </Button>
+        <button type="button" className="btn btn-primary" onClick={handleSubmit}>
+          Done
+        </button>
       </ModalFooter>
       </ModalFooter>
     </Modal>
     </Modal>
   );
   );

+ 19 - 11
src/client/js/components/Page/TagLabels.jsx

@@ -28,11 +28,12 @@ class TagLabels extends React.Component {
 
 
   /**
   /**
    * @return tags data
    * @return tags data
-   *   1. pageContainer.state.tags if pageId is not null
-   *   2. editorContainer.state.tags if pageId is null
+   *   1. pageContainer.state.tags if editorMode is view
+   *   2. editorContainer.state.tags if editorMode is edit
    */
    */
-  getEditTargetData() {
-    return (this.props.editorContainer.state.pageId != null) ? this.props.editorContainer.state.tags : this.props.pageContainer.state.tags;
+  getTagData() {
+    const { editorContainer, pageContainer, editorMode } = this.props;
+    return (editorMode === 'edit') ? editorContainer.state.tags : pageContainer.state.tags;
   }
   }
 
 
   openEditorModal() {
   openEditorModal() {
@@ -43,20 +44,26 @@ class TagLabels extends React.Component {
     this.setState({ isTagEditModalShown: false });
     this.setState({ isTagEditModalShown: false });
   }
   }
 
 
-  async tagsUpdatedHandler(tags) {
-    const { appContainer, editorContainer, pageContainer } = this.props;
+  async tagsUpdatedHandler(newTags) {
+    const {
+      appContainer, editorContainer, pageContainer, editorMode,
+    } = this.props;
+
     const { pageId } = pageContainer.state;
     const { pageId } = pageContainer.state;
 
 
-    // only update tags in editorContainer when new page
-    if (pageId != null) {
-      return editorContainer.setState({ tags });
+    // It will not be reflected in the DB until the page is refreshed
+    if (editorMode === 'edit') {
+      return editorContainer.setState({ tags: newTags });
     }
     }
 
 
     try {
     try {
-      await appContainer.apiPost('/tags.update', { pageId, tags });
+      const { tags } = await appContainer.apiPost('/tags.update', { pageId, tags: newTags });
 
 
       // update pageContainer.state
       // update pageContainer.state
       pageContainer.setState({ tags });
       pageContainer.setState({ tags });
+      // update editorContainer.state
+      editorContainer.setState({ tags });
+
       toastSuccess('updated tags successfully');
       toastSuccess('updated tags successfully');
     }
     }
     catch (err) {
     catch (err) {
@@ -66,7 +73,7 @@ class TagLabels extends React.Component {
 
 
 
 
   render() {
   render() {
-    const tags = this.getEditTargetData();
+    const tags = this.getTagData();
 
 
     return (
     return (
       <>
       <>
@@ -107,6 +114,7 @@ TagLabels.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
   editorContainer: PropTypes.instanceOf(EditorContainer).isRequired,
 
 
+  editorMode: PropTypes.string.isRequired,
 };
 };
 
 
 export default withTranslation()(TagLabelsWrapper);
 export default withTranslation()(TagLabelsWrapper);

+ 57 - 99
src/client/js/components/PageAccessoriesModal.jsx

@@ -1,15 +1,14 @@
-import React, { useEffect, useState } from 'react';
+import React, { useCallback, useMemo } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import {
 import {
-  Modal, ModalBody, ModalHeader, Nav, NavItem, NavLink, TabContent, TabPane,
+  Modal, ModalBody, ModalHeader, TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
-
 import PageListIcon from './Icons/PageListIcon';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
-import RecentChangesIcon from './Icons/RecentChangesIcon';
+import HistoryIcon from './Icons/HistoryIcon';
 import AttachmentIcon from './Icons/AttachmentIcon';
 import AttachmentIcon from './Icons/AttachmentIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 
 
@@ -20,123 +19,82 @@ import PageTimeline from './PageTimeline';
 import PageList from './PageList';
 import PageList from './PageList';
 import PageHistory from './PageHistory';
 import PageHistory from './PageHistory';
 import ShareLink from './ShareLink/ShareLink';
 import ShareLink from './ShareLink/ShareLink';
-
-
-const navTabMapping = {
-  pagelist: {
-    icon: <PageListIcon />,
-    i18n: 'page_list',
-    index: 0,
-  },
-  timeline:  {
-    icon: <TimeLineIcon />,
-    i18n: 'Timeline View',
-    index: 1,
-  },
-  pageHistory: {
-    icon: <RecentChangesIcon />,
-    i18n: 'History',
-    index: 2,
-  },
-  attachment: {
-    icon: <AttachmentIcon />,
-    i18n: 'attachment_data',
-    index: 3,
-  },
-  shareLink: {
-    icon: <ShareLinkIcon />,
-    i18n: 'share_links.share_link_management',
-    index: 4,
-  },
-};
+import { CustomNav } from './CustomNavigation';
 
 
 const PageAccessoriesModal = (props) => {
 const PageAccessoriesModal = (props) => {
-  const { t, pageAccessoriesContainer } = props;
+  const {
+    t, pageAccessoriesContainer, onClose, isGuestUserMode,
+  } = props;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { switchActiveTab } = pageAccessoriesContainer;
-  const { activeTab } = pageAccessoriesContainer.state;
-
-  const [sliderWidth, setSliderWidth] = useState(null);
-  const [sliderMarginLeft, setSliderMarginLeft] = useState(null);
-
-  function closeModalHandler() {
-    if (props.onClose == null) {
+  const { activeTab, activeComponents } = pageAccessoriesContainer.state;
+
+  const navTabMapping = useMemo(() => {
+    return {
+      pagelist: {
+        Icon: PageListIcon,
+        i18n: t('page_list'),
+        index: 0,
+      },
+      timeline:  {
+        Icon: TimeLineIcon,
+        i18n: t('Timeline View'),
+        index: 1,
+      },
+      pageHistory: {
+        Icon: HistoryIcon,
+        i18n: t('History'),
+        index: 2,
+      },
+      attachment: {
+        Icon: AttachmentIcon,
+        i18n: t('attachment_data'),
+        index: 3,
+      },
+      shareLink: {
+        Icon: ShareLinkIcon,
+        i18n: t('share_links.share_link_management'),
+        index: 4,
+        isLinkEnabled: v => !isGuestUserMode,
+      },
+    };
+  }, [t, isGuestUserMode]);
+
+  const closeModalHandler = useCallback(() => {
+    if (onClose == null) {
       return;
       return;
     }
     }
-    props.onClose();
-  }
-
-  // Might make this dynamic for px, %, pt, em
-  function getPercentage(min, max) {
-    return min / max * 100;
-  }
-
-  useEffect(() => {
-    if (activeTab === '') {
-      return;
-    }
-
-    const navTitle = document.getElementById('nav-title');
-    const navTabs = document.querySelectorAll('li.nav-link');
-
-    if (navTitle == null || navTabs == null) {
-      return;
-    }
-
-    let tempML = 0;
-
-    const styles = [].map.call(navTabs, (el) => {
-      const width = getPercentage(el.offsetWidth, navTitle.offsetWidth);
-      const marginLeft = tempML;
-      tempML += width;
-      return { width, marginLeft };
-    });
-
-    const { width, marginLeft } = styles[navTabMapping[activeTab].index];
-
-    setSliderWidth(width);
-    setSliderMarginLeft(marginLeft);
-
-  }, [activeTab]);
-
+    onClose();
+  }, [onClose]);
 
 
   return (
   return (
     <React.Fragment>
     <React.Fragment>
       <Modal size="xl" isOpen={props.isOpen} toggle={closeModalHandler} className="grw-page-accessories-modal">
       <Modal size="xl" isOpen={props.isOpen} toggle={closeModalHandler} className="grw-page-accessories-modal">
-        {/* [TODO: insert a modal header and move nav tabs there  by gw-3890] */}
         <ModalHeader className="p-0" toggle={closeModalHandler}>
         <ModalHeader className="p-0" toggle={closeModalHandler}>
-          <Nav className="nav-title" id="nav-title">
-            {Object.entries(navTabMapping).map(([key, value]) => {
-              return (
-                <NavItem key={key} type="button" className={`p-0 nav-link ${activeTab === key && 'active'}`}>
-                  <NavLink onClick={() => { switchActiveTab(key) }}>
-                    {value.icon}
-                    {t(value.i18n)}
-                  </NavLink>
-                </NavItem>
-              );
-            })}
-          </Nav>
-          <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
+          <CustomNav activeTab={activeTab} navTabMapping={navTabMapping} onNavSelected={switchActiveTab} />
         </ModalHeader>
         </ModalHeader>
         <ModalBody className="overflow-auto grw-modal-body-style p-0">
         <ModalBody className="overflow-auto grw-modal-body-style p-0">
+          {/* Do not use CustomTabContent because of performance problem:
+              the 'navTabMapping[tabId].Content' for PageAccessoriesModal depends on activeComponents */}
           <TabContent activeTab={activeTab} className="p-5">
           <TabContent activeTab={activeTab} className="p-5">
             <TabPane tabId="pagelist">
             <TabPane tabId="pagelist">
-              {pageAccessoriesContainer.state.activeComponents.has('pagelist') && <PageList />}
+              {activeComponents.has('pagelist') && <PageList />}
             </TabPane>
             </TabPane>
             <TabPane tabId="timeline">
             <TabPane tabId="timeline">
-              {pageAccessoriesContainer.state.activeComponents.has('timeline') && <PageTimeline /> }
+              {activeComponents.has('timeline') && <PageTimeline /> }
             </TabPane>
             </TabPane>
             <TabPane tabId="pageHistory">
             <TabPane tabId="pageHistory">
               <div className="overflow-auto">
               <div className="overflow-auto">
-                {pageAccessoriesContainer.state.activeComponents.has('pageHistory') && <PageHistory /> }
+                {activeComponents.has('pageHistory') && <PageHistory /> }
               </div>
               </div>
             </TabPane>
             </TabPane>
             <TabPane tabId="attachment">
             <TabPane tabId="attachment">
-              {pageAccessoriesContainer.state.activeComponents.has('attachment') && <PageAttachment />}
-            </TabPane>
-            <TabPane tabId="shareLink">
-              {pageAccessoriesContainer.state.activeComponents.has('shareLink') && <ShareLink />}
+              {activeComponents.has('attachment') && <PageAttachment />}
             </TabPane>
             </TabPane>
+            {!isGuestUserMode && (
+              <TabPane tabId="shareLink">
+                {activeComponents.has('shareLink') && <ShareLink />}
+              </TabPane>
+            )}
           </TabContent>
           </TabContent>
         </ModalBody>
         </ModalBody>
       </Modal>
       </Modal>
@@ -151,8 +109,8 @@ const PageAccessoriesModalWrapper = withUnstatedContainers(PageAccessoriesModal,
 
 
 PageAccessoriesModal.propTypes = {
 PageAccessoriesModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  // pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
+  isGuestUserMode: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func,
   onClose: PropTypes.func,
 };
 };

+ 17 - 16
src/client/js/components/PageComment/Comment.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
+import { withTranslation } from 'react-i18next';
 import { format } from 'date-fns';
 import { format } from 'date-fns';
 
 
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
@@ -16,6 +17,7 @@ import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
 import Username from '../User/Username';
 import CommentEditor from './CommentEditor';
 import CommentEditor from './CommentEditor';
 import CommentControl from './CommentControl';
 import CommentControl from './CommentControl';
+import HistoryIcon from '../Icons/HistoryIcon';
 
 
 /**
 /**
  *
  *
@@ -38,7 +40,6 @@ class Comment extends React.PureComponent {
     this.isCurrentUserIsAuthor = this.isCurrentUserEqualsToAuthor.bind(this);
     this.isCurrentUserIsAuthor = this.isCurrentUserEqualsToAuthor.bind(this);
     this.isCurrentRevision = this.isCurrentRevision.bind(this);
     this.isCurrentRevision = this.isCurrentRevision.bind(this);
     this.getRootClassName = this.getRootClassName.bind(this);
     this.getRootClassName = this.getRootClassName.bind(this);
-    this.getRevisionLabelClassName = this.getRevisionLabelClassName.bind(this);
     this.deleteBtnClickedHandler = this.deleteBtnClickedHandler.bind(this);
     this.deleteBtnClickedHandler = this.deleteBtnClickedHandler.bind(this);
     this.renderText = this.renderText.bind(this);
     this.renderText = this.renderText.bind(this);
     this.renderHtml = this.renderHtml.bind(this);
     this.renderHtml = this.renderHtml.bind(this);
@@ -109,11 +110,6 @@ class Comment extends React.PureComponent {
     return className;
     return className;
   }
   }
 
 
-  getRevisionLabelClassName() {
-    return `page-comment-revision badge ${
-      this.isCurrentRevision() ? 'badge-primary' : 'badge-secondary'}`;
-  }
-
   deleteBtnClickedHandler() {
   deleteBtnClickedHandler() {
     this.props.deleteBtnClicked(this.props.comment);
     this.props.deleteBtnClicked(this.props.comment);
   }
   }
@@ -155,6 +151,7 @@ class Comment extends React.PureComponent {
   }
   }
 
 
   render() {
   render() {
+    const { t } = this.props;
     const comment = this.props.comment;
     const comment = this.props.comment;
     const commentId = comment._id;
     const commentId = comment._id;
     const creator = comment.creator;
     const creator = comment.creator;
@@ -166,8 +163,6 @@ class Comment extends React.PureComponent {
     const rootClassName = this.getRootClassName(comment);
     const rootClassName = this.getRootClassName(comment);
     const commentBody = isMarkdown ? this.renderRevisionBody() : this.renderText(comment.comment);
     const commentBody = isMarkdown ? this.renderRevisionBody() : this.renderText(comment.comment);
     const revHref = `?revision=${comment.revision}`;
     const revHref = `?revision=${comment.revision}`;
-    const revFirst8Letters = comment.revision.substr(-8);
-    const revisionLavelClassName = this.getRevisionLabelClassName();
 
 
     const editedDateId = `editedDate-${comment._id}`;
     const editedDateId = `editedDate-${comment._id}`;
     const editedDateFormatted = isEdited
     const editedDateFormatted = isEdited
@@ -176,7 +171,6 @@ class Comment extends React.PureComponent {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-
         {this.state.isReEdit ? (
         {this.state.isReEdit ? (
           <CommentEditor
           <CommentEditor
             growiRenderer={this.props.growiRenderer}
             growiRenderer={this.props.growiRenderer}
@@ -206,10 +200,17 @@ class Comment extends React.PureComponent {
                     <span id={editedDateId}>&nbsp;(edited)</span>
                     <span id={editedDateId}>&nbsp;(edited)</span>
                     <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
                     <UncontrolledTooltip placement="bottom" fade={false} target={editedDateId}>{editedDateFormatted}</UncontrolledTooltip>
                   </>
                   </>
-                ) }
-                <span className="ml-2"><a className={revisionLavelClassName} href={revHref}>{revFirst8Letters}</a></span>
+                )}
+                <span className="ml-2">
+                  <a id={`page-comment-revision-${commentId}`} className="page-comment-revision" href={revHref}>
+                    <HistoryIcon />
+                  </a>
+                  <UncontrolledTooltip placement="bottom" fade={false} target={`page-comment-revision-${commentId}`}>
+                    {t('page_comment.display_the_page_when_posting_this_comment')}
+                  </UncontrolledTooltip>
+                </span>
               </div>
               </div>
-              { this.checkPermissionToControlComment() && (
+              {this.checkPermissionToControlComment() && (
                 <CommentControl
                 <CommentControl
                   onClickDeleteBtn={this.deleteBtnClickedHandler}
                   onClickDeleteBtn={this.deleteBtnClickedHandler}
                   onClickEditBtn={() => this.setState({ isReEdit: true })}
                   onClickEditBtn={() => this.setState({ isReEdit: true })}
@@ -217,9 +218,8 @@ class Comment extends React.PureComponent {
               ) }
               ) }
             </div>
             </div>
           </div>
           </div>
-          )
-        }
-
+        )
+      }
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }
@@ -232,6 +232,7 @@ class Comment extends React.PureComponent {
 const CommentWrapper = withUnstatedContainers(Comment, [AppContainer, PageContainer]);
 const CommentWrapper = withUnstatedContainers(Comment, [AppContainer, PageContainer]);
 
 
 Comment.propTypes = {
 Comment.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
@@ -240,4 +241,4 @@ Comment.propTypes = {
   deleteBtnClicked: PropTypes.func.isRequired,
   deleteBtnClicked: PropTypes.func.isRequired,
 };
 };
 
 
-export default CommentWrapper;
+export default withTranslation()(CommentWrapper);

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

@@ -170,7 +170,7 @@ class PageComments extends React.Component {
               className="btn-comment-reply"
               className="btn-comment-reply"
               onClick={() => { return this.replyButtonClickedHandler(commentId) }}
               onClick={() => { return this.replyButtonClickedHandler(commentId) }}
             >
             >
-              <i className="icon-fw icon-action-redo"></i> Reply
+              <i className="icon-fw icon-action-undo"></i> Reply
             </Button>
             </Button>
           </div>
           </div>
         )}
         )}

+ 3 - 3
src/client/js/components/PageList.jsx

@@ -15,7 +15,7 @@ const PageList = (props) => {
   const { appContainer, pageContainer, t } = props;
   const { appContainer, pageContainer, t } = props;
   const { path } = pageContainer.state;
   const { path } = pageContainer.state;
   const [pages, setPages] = useState(null);
   const [pages, setPages] = useState(null);
-  const [isLoading, setIsLoading] = useState(false);
+  const [isLoading, setIsLoading] = useState(true);
 
 
   const [activePage, setActivePage] = useState(1);
   const [activePage, setActivePage] = useState(1);
   const [totalPages, setTotalPages] = useState(0);
   const [totalPages, setTotalPages] = useState(0);
@@ -30,7 +30,7 @@ const PageList = (props) => {
     const res = await appContainer.apiv3Get('/pages/list', { path, page });
     const res = await appContainer.apiv3Get('/pages/list', { path, page });
 
 
     setPages(res.data.pages);
     setPages(res.data.pages);
-    setIsLoading(true);
+    setIsLoading(false);
     setTotalPages(res.data.totalCount);
     setTotalPages(res.data.totalCount);
     setLimit(res.data.limit);
     setLimit(res.data.limit);
   }, [appContainer, path, activePage]);
   }, [appContainer, path, activePage]);
@@ -40,7 +40,7 @@ const PageList = (props) => {
   }, [updatePageList]);
   }, [updatePageList]);
 
 
 
 
-  if (isLoading === false) {
+  if (isLoading) {
     return (
     return (
       <div className="wiki">
       <div className="wiki">
         <div className="text-muted test-center">
         <div className="text-muted test-center">

+ 97 - 0
src/client/js/components/PageList/BookmarkList.jsx

@@ -0,0 +1,97 @@
+import React, { useState, useCallback, useEffect } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+import loggerFactory from '@alias/logger';
+import { withUnstatedContainers } from '../UnstatedUtils';
+
+
+import AppContainer from '../../services/AppContainer';
+import { toastError } from '../../util/apiNotification';
+
+import PaginationWrapper from '../PaginationWrapper';
+
+import Page from './Page';
+
+const logger = loggerFactory('growi:BookmarkList');
+
+const BookmarkList = (props) => {
+  const { t, appContainer, userId } = props;
+
+  const [pages, setPages] = useState([]);
+
+  const [activePage, setActivePage] = useState(1);
+  const [totalItemsCount, setTotalItemsCount] = useState(0);
+  const [pagingLimit, setPagingLimit] = useState(10);
+
+  const setPageNumber = (selectedPageNumber) => {
+    setActivePage(selectedPageNumber);
+  };
+
+  const getMyBookmarkList = useCallback(async() => {
+    const page = activePage;
+
+    try {
+      const res = await appContainer.apiv3Get(`/bookmarks/${userId}`, { page });
+      const { paginationResult } = res.data;
+
+      setPages(paginationResult.docs);
+      setTotalItemsCount(paginationResult.totalDocs);
+      setPagingLimit(paginationResult.limit);
+    }
+    catch (error) {
+      logger.error('failed to fetch data', error);
+      toastError(error, 'Error occurred in bookmark page list');
+    }
+  }, [appContainer, activePage, userId]);
+
+  useEffect(() => {
+    getMyBookmarkList();
+  }, [getMyBookmarkList]);
+
+  /**
+   * generate Elements of Page
+   *
+   * @param {any} pages Array of pages Model Obj
+   *
+   */
+  const generatePageList = pages.map(page => (
+    <li key={`my-bookmarks:${page._id}`} className="mt-4">
+      <Page page={page.page} />
+    </li>
+  ));
+
+  return (
+    <div className="bookmarks-list-container">
+      {pages.length === 0 ? t('No bookmarks yet') : (
+        <>
+          <ul className="page-list-ul page-list-ul-flat mb-3">
+            {generatePageList}
+          </ul>
+          <PaginationWrapper
+            activePage={activePage}
+            changePage={setPageNumber}
+            totalItemsCount={totalItemsCount}
+            pagingLimit={pagingLimit}
+            align="center"
+            size="sm"
+          />
+        </>
+      )}
+    </div>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const BookmarkListWrapper = withUnstatedContainers(BookmarkList, [AppContainer]);
+
+BookmarkList.propTypes = {
+  t: PropTypes.func.isRequired,
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  userId: PropTypes.string.isRequired,
+};
+
+export default withTranslation()(BookmarkListWrapper);

+ 3 - 9
src/client/js/components/PaginationWrapper.jsx

@@ -5,9 +5,6 @@ import { withTranslation } from 'react-i18next';
 
 
 import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
 import { Pagination, PaginationItem, PaginationLink } from 'reactstrap';
 
 
-import { withUnstatedContainers } from './UnstatedUtils';
-import AppContainer from '../services/AppContainer';
-
 class PaginationWrapper extends React.Component {
 class PaginationWrapper extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
@@ -181,22 +178,19 @@ class PaginationWrapper extends React.Component {
 
 
 }
 }
 
 
-const PaginationWrappered = withUnstatedContainers(PaginationWrapper, [AppContainer]);
-
 PaginationWrapper.propTypes = {
 PaginationWrapper.propTypes = {
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
 
 
   activePage: PropTypes.number.isRequired,
   activePage: PropTypes.number.isRequired,
   changePage: PropTypes.func.isRequired,
   changePage: PropTypes.func.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
-  pagingLimit: PropTypes.number,
+  pagingLimit: PropTypes.number.isRequired,
   align: PropTypes.string,
   align: PropTypes.string,
   size: PropTypes.string,
   size: PropTypes.string,
 };
 };
+
 PaginationWrapper.defaultProps = {
 PaginationWrapper.defaultProps = {
   align: 'left',
   align: 'left',
   size: 'md',
   size: 'md',
-  pagingLimit: PropTypes.number,
 };
 };
 
 
-export default withTranslation()(PaginationWrappered);
+export default withTranslation()(PaginationWrapper);

+ 6 - 7
src/client/js/components/RecentCreated/RecentCreated.jsx

@@ -4,9 +4,8 @@ import PropTypes from 'prop-types';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
 
 
-import PaginationWrapper from '../PaginationWrapper';
-
 import Page from '../PageList/Page';
 import Page from '../PageList/Page';
+import PaginationWrapper from '../PaginationWrapper';
 
 
 class RecentCreated extends React.Component {
 class RecentCreated extends React.Component {
 
 
@@ -17,7 +16,7 @@ class RecentCreated extends React.Component {
       pages: [],
       pages: [],
       activePage: 1,
       activePage: 1,
       totalPages: 0,
       totalPages: 0,
-      pagingLimit: null,
+      pagingLimit: 10,
     };
     };
 
 
     this.handlePage = this.handlePage.bind(this);
     this.handlePage = this.handlePage.bind(this);
@@ -35,7 +34,6 @@ class RecentCreated extends React.Component {
   async getRecentCreatedList(selectedPage) {
   async getRecentCreatedList(selectedPage) {
     const { appContainer, userId } = this.props;
     const { appContainer, userId } = this.props;
     const page = selectedPage;
     const page = selectedPage;
-    // const userId = appContainer.currentUserId;
 
 
     // pagesList get and pagination calculate
     // pagesList get and pagination calculate
     const res = await appContainer.apiv3Get(`/users/${userId}/recent`, { page });
     const res = await appContainer.apiv3Get(`/users/${userId}/recent`, { page });
@@ -58,7 +56,7 @@ class RecentCreated extends React.Component {
    */
    */
   generatePageList(pages) {
   generatePageList(pages) {
     return pages.map(page => (
     return pages.map(page => (
-      <li key={`recent-created:list-view:${page._id}`}>
+      <li key={`recent-created:list-view:${page._id}`} className="mt-4">
         <Page page={page} />
         <Page page={page} />
       </li>
       </li>
     ));
     ));
@@ -68,11 +66,12 @@ class RecentCreated extends React.Component {
     const pageList = this.generatePageList(this.state.pages);
     const pageList = this.generatePageList(this.state.pages);
 
 
     return (
     return (
-      <div>
-        <ul className="page-list-ul page-list-ul-flat mb-3">
+      <div className="page-list-container-create">
+        <ul className="page-list-ul page-list-ul-flat">
           {pageList}
           {pageList}
         </ul>
         </ul>
         <PaginationWrapper
         <PaginationWrapper
+          align="center"
           activePage={this.state.activePage}
           activePage={this.state.activePage}
           changePage={this.handlePage}
           changePage={this.handlePage}
           totalItemsCount={this.state.totalPages}
           totalItemsCount={this.state.totalPages}

+ 1 - 1
src/client/js/components/Sidebar/SidebarNav.jsx

@@ -71,7 +71,7 @@ class SidebarNav extends React.Component {
         </div>
         </div>
         <div className="grw-sidebar-nav-secondary-container">
         <div className="grw-sidebar-nav-secondary-container">
           {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
           {isAdmin && <SecondaryItem label="Admin" iconName="settings" href="/admin" />}
-          {isLoggedIn && <SecondaryItem label="Draft" iconName="file_copy" href={`/user/${currentUsername}#user-draft-list`} />}
+          {isLoggedIn && <SecondaryItem label="Draft" iconName="file_copy" href="/me/drafts" />}
           <SecondaryItem label="Help" iconName="help" href="https://docs.growi.org" isBlank />
           <SecondaryItem label="Help" iconName="help" href="https://docs.growi.org" isBlank />
           {isLoggedIn && <SecondaryItem label="Trash" iconName="delete" href="/trash" />}
           {isLoggedIn && <SecondaryItem label="Trash" iconName="delete" href="/trash" />}
         </div>
         </div>

+ 41 - 6
src/client/js/components/TableOfContents.jsx

@@ -1,4 +1,4 @@
-import React, { useCallback, useEffect } from 'react';
+import React, { useCallback, useEffect, useMemo } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
@@ -11,8 +11,11 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import TopOfTableContents from './TopOfTableContents';
 import TopOfTableContents from './TopOfTableContents';
 import StickyStretchableScroller from './StickyStretchableScroller';
 import StickyStretchableScroller from './StickyStretchableScroller';
 
 
+import RecentlyCreatedIcon from './Icons/RecentlyCreatedIcon';
+
 // eslint-disable-next-line no-unused-vars
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:TableOfContents');
 const logger = loggerFactory('growi:TableOfContents');
+const WIKI_HEADER_LINK = 120;
 
 
 /**
 /**
  * @author Yuki Takei <yuki@weseek.co.jp>
  * @author Yuki Takei <yuki@weseek.co.jp>
@@ -20,7 +23,9 @@ const logger = loggerFactory('growi:TableOfContents');
  */
  */
 const TableOfContents = (props) => {
 const TableOfContents = (props) => {
 
 
-  const { pageContainer, navigationContainer } = props;
+  const { pageContainer, navigationContainer, isGuestUserMode } = props;
+  const { pageUser } = pageContainer.state;
+  const isUserPage = pageUser != null;
 
 
   const calcViewHeight = useCallback(() => {
   const calcViewHeight = useCallback(() => {
     // calculate absolute top of '#revision-toc' element
     // calculate absolute top of '#revision-toc' element
@@ -28,8 +33,11 @@ const TableOfContents = (props) => {
     const containerTop = containerElem.getBoundingClientRect().top;
     const containerTop = containerElem.getBoundingClientRect().top;
 
 
     // window height - revisionToc top - .system-version - .grw-fab-container height - top-of-table-contents height
     // window height - revisionToc top - .system-version - .grw-fab-container height - top-of-table-contents height
+    if (isUserPage) {
+      return window.innerHeight - containerTop - 20 - 155 - 26 - 40;
+    }
     return window.innerHeight - containerTop - 20 - 155 - 26;
     return window.innerHeight - containerTop - 20 - 155 - 26;
-  }, []);
+  }, [isUserPage]);
 
 
   const { tocHtml } = pageContainer.state;
   const { tocHtml } = pageContainer.state;
 
 
@@ -40,9 +48,13 @@ const TableOfContents = (props) => {
     navigationContainer.addSmoothScrollEvent(anchorsInToc);
     navigationContainer.addSmoothScrollEvent(anchorsInToc);
   }, [tocHtml, navigationContainer]);
   }, [tocHtml, navigationContainer]);
 
 
+  // get element for smoothScroll
+  const getBookMarkListHeaderDom = useMemo(() => { return document.getElementById('bookmarks-list') }, []);
+  const getRecentlyCreatedListHeaderDom = useMemo(() => { return document.getElementById('recently-created-list') }, []);
+
   return (
   return (
     <>
     <>
-      <TopOfTableContents />
+      <TopOfTableContents isGuestUserMode={isGuestUserMode} />
       <StickyStretchableScroller
       <StickyStretchableScroller
         contentsElemSelector=".revision-toc .markdownIt-TOC"
         contentsElemSelector=".revision-toc .markdownIt-TOC"
         stickyElemSelector="#revision-toc"
         stickyElemSelector="#revision-toc"
@@ -50,13 +62,34 @@ const TableOfContents = (props) => {
       >
       >
         <div
         <div
           id="revision-toc-content"
           id="revision-toc-content"
-          className="revision-toc-content"
-        // eslint-disable-next-line react/no-danger
+          className="revision-toc-content top-of-table-contents"
+         // eslint-disable-next-line react/no-danger
           dangerouslySetInnerHTML={{
           dangerouslySetInnerHTML={{
           __html: tocHtml,
           __html: tocHtml,
         }}
         }}
         />
         />
       </StickyStretchableScroller>
       </StickyStretchableScroller>
+
+      { isUserPage && (
+      <div className="mt-3 d-flex justify-content-around">
+        <button
+          type="button"
+          className="btn btn-outline-secondary btn-sm"
+          onClick={() => navigationContainer.smoothScrollIntoView(getBookMarkListHeaderDom, WIKI_HEADER_LINK)}
+        >
+          <i className="mr-2 icon-star"></i>
+          <span>Bookmarks</span>
+        </button>
+        <button
+          type="button"
+          className="btn btn-outline-secondary btn-sm"
+          onClick={() => navigationContainer.smoothScrollIntoView(getRecentlyCreatedListHeaderDom, WIKI_HEADER_LINK)}
+        >
+          <i className="grw-icon-container-recently-created mr-2"><RecentlyCreatedIcon /></i>
+          <span>Recently Created</span>
+        </button>
+      </div>
+      )}
     </>
     </>
   );
   );
 
 
@@ -70,6 +103,8 @@ const TableOfContentsWrapper = withUnstatedContainers(TableOfContents, [PageCont
 TableOfContents.propTypes = {
 TableOfContents.propTypes = {
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
+
+  isGuestUserMode: PropTypes.bool.isRequired,
 };
 };
 
 
 export default withTranslation()(TableOfContentsWrapper);
 export default withTranslation()(TableOfContentsWrapper);

+ 27 - 17
src/client/js/components/TopOfTableContents.jsx

@@ -3,11 +3,12 @@ import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { UncontrolledTooltip } from 'reactstrap';
 import PageAccessoriesContainer from '../services/PageAccessoriesContainer';
 import PageAccessoriesContainer from '../services/PageAccessoriesContainer';
 
 
 import PageListIcon from './Icons/PageListIcon';
 import PageListIcon from './Icons/PageListIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
 import TimeLineIcon from './Icons/TimeLineIcon';
-import RecentChangesIcon from './Icons/RecentChangesIcon';
+import HistoryIcon from './Icons/HistoryIcon';
 import AttachmentIcon from './Icons/AttachmentIcon';
 import AttachmentIcon from './Icons/AttachmentIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 import ShareLinkIcon from './Icons/ShareLinkIcon';
 
 
@@ -16,16 +17,15 @@ import PageAccessoriesModal from './PageAccessoriesModal';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 const TopOfTableContents = (props) => {
 const TopOfTableContents = (props) => {
-  const { pageAccessoriesContainer } = props;
+  const { t, pageAccessoriesContainer, isGuestUserMode } = props;
 
 
   function renderModal() {
   function renderModal() {
     return (
     return (
-      <>
-        <PageAccessoriesModal
-          isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
-          onClose={pageAccessoriesContainer.closePageAccessoriesModal}
-        />
-      </>
+      <PageAccessoriesModal
+        isGuestUserMode={isGuestUserMode}
+        isOpen={pageAccessoriesContainer.state.isPageAccessoriesModalShown}
+        onClose={pageAccessoriesContainer.closePageAccessoriesModal}
+      />
     );
     );
   }
   }
 
 
@@ -53,7 +53,7 @@ const TopOfTableContents = (props) => {
           className="btn btn-link grw-btn-top-of-table"
           className="btn btn-link grw-btn-top-of-table"
           onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pageHistory')}
           onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pageHistory')}
         >
         >
-          <RecentChangesIcon />
+          <HistoryIcon />
         </button>
         </button>
 
 
         <button
         <button
@@ -64,14 +64,20 @@ const TopOfTableContents = (props) => {
           <AttachmentIcon />
           <AttachmentIcon />
         </button>
         </button>
 
 
-        <button
-          type="button"
-          className="btn btn-link grw-btn-top-of-table"
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('shareLink')}
-        >
-          <ShareLinkIcon />
-        </button>
-
+        <div id="shareLink-btn-wrapper-for-tooltip">
+          <button
+            type="button"
+            className={`btn btn-link grw-btn-top-of-table ${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>
+        )}
         <div
         <div
           id="seen-user-list"
           id="seen-user-list"
           data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
           data-user-ids-str="{{ page.seenUsers|slice(-15)|default([])|reverse|join(',') }}"
@@ -90,7 +96,11 @@ const TopOfTableContents = (props) => {
 const TopOfTableContentsWrapper = withUnstatedContainers(TopOfTableContents, [PageAccessoriesContainer]);
 const TopOfTableContentsWrapper = withUnstatedContainers(TopOfTableContents, [PageAccessoriesContainer]);
 
 
 TopOfTableContents.propTypes = {
 TopOfTableContents.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
   pageAccessoriesContainer: PropTypes.instanceOf(PageAccessoriesContainer).isRequired,
+
+  isGuestUserMode: PropTypes.bool.isRequired,
 };
 };
 
 
 export default withTranslation()(TopOfTableContentsWrapper);
 export default withTranslation()(TopOfTableContentsWrapper);

+ 2 - 2
src/client/js/components/TrashPageList.jsx

@@ -11,9 +11,9 @@ const TrashPageList = (props) => {
 
 
   const navTabMapping = {
   const navTabMapping = {
     pagelist: {
     pagelist: {
-      icon: <PageListIcon />,
+      Icon: PageListIcon,
+      Content: PageList,
       i18n: t('page_list'),
       i18n: t('page_list'),
-      tabContent: <PageList />,
       index: 0,
       index: 0,
     },
     },
   };
   };

+ 41 - 0
src/client/js/components/User/UserInfo.jsx

@@ -0,0 +1,41 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import UserPicture from './UserPicture';
+
+const UserInfo = (props) => {
+  const { pageUser } = props;
+
+  // do not display when the user does not exist
+  if (pageUser == null) {
+    return null;
+  }
+
+  return (
+    <div className="grw-users-info d-flex align-items-center d-edit-none pb-2 border-bottom">
+      <UserPicture user={pageUser} />
+
+      <div className="users-meta">
+        <h1 className="user-page-name">
+          {pageUser.name}
+        </h1>
+        <div className="user-page-meta mt-3 mb-0">
+          <span className="user-page-username mr-4"><i className="icon-user mr-1"></i>{pageUser.username}</span>
+          <span className="user-page-email mr-2">
+            <i className="icon-envelope mr-1"></i>
+            {pageUser.isEmailPublished ? pageUser.email : '*****'}
+          </span>
+          {pageUser.introduction && <span className="user-page-introduction">{pageUser.introduction}</span>}
+        </div>
+      </div>
+
+    </div>
+  );
+};
+
+
+UserInfo.propTypes = {
+  pageUser: PropTypes.object,
+};
+
+export default UserInfo;

+ 0 - 5
src/client/js/legacy/crowi.js

@@ -202,11 +202,6 @@ $(() => {
 window.addEventListener('load', (e) => {
 window.addEventListener('load', (e) => {
   const { appContainer } = window;
   const { appContainer } = window;
 
 
-  // do nothing if user is guest
-  if (appContainer.currentUser == null) {
-    return;
-  }
-
   // hash on page
   // hash on page
   if (window.location.hash) {
   if (window.location.hash) {
     const navigationContainer = appContainer.getContainer('NavigationContainer');
     const navigationContainer = appContainer.getContainer('NavigationContainer');

+ 9 - 0
src/client/js/services/NavigationContainer.js

@@ -1,4 +1,7 @@
 import { Container } from 'unstated';
 import { Container } from 'unstated';
+import loggerFactory from '@alias/logger';
+
+const logger = loggerFactory('growi:services:NavigationContainer');
 
 
 /**
 /**
  * Service container related to options for Application
  * Service container related to options for Application
@@ -86,6 +89,12 @@ export default class NavigationContainer extends Container {
   }
   }
 
 
   setEditorMode(editorMode) {
   setEditorMode(editorMode) {
+
+    if (this.appContainer.currentUser == null) {
+      logger.warn('Please login or signup to edit the page or use hackmd.');
+      return;
+    }
+
     this.setState({ editorMode });
     this.setState({ editorMode });
     if (editorMode === 'view') {
     if (editorMode === 'view') {
       $('body').removeClass('on-edit');
       $('body').removeClass('on-edit');

+ 2 - 2
src/client/js/services/PageContainer.js

@@ -58,10 +58,10 @@ 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'),
-      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')),
-      isCreatable: JSON.parse(mainContent.getAttribute('data-page-is-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')),
       pageUser: JSON.parse(mainContent.getAttribute('data-page-user')),
       pageUser: JSON.parse(mainContent.getAttribute('data-page-user')),
       tags: null,
       tags: null,

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

@@ -40,6 +40,11 @@
       font-size: 0.9em;
       font-size: 0.9em;
       color: $gray-400;
       color: $gray-400;
     }
     }
+
+    .page-comment-revision svg {
+      width: 16px;
+      height: 16px;
+    }
   }
   }
 
 
   .page-comment-main {
   .page-comment-main {

+ 3 - 8
src/client/styles/scss/_comment_growi.scss

@@ -33,8 +33,8 @@
   }
   }
 
 
   .page-comments-row {
   .page-comments-row {
-    margin: 30px 0px;
-    border-top: 5px solid;
+    // offset margin left to apply bg-color
+    margin: 30px -15px 30px -15px;
   }
   }
 
 
   .page-comments {
   .page-comments {
@@ -98,13 +98,8 @@
   }
   }
   // reply button
   // reply button
   .btn.btn-comment-reply {
   .btn.btn-comment-reply {
-    width: 120px;
     margin-top: 0.5em;
     margin-top: 0.5em;
-    margin-right: 15px;
-
-    border-top: none;
-    border-right: none;
-    border-left: none;
+    border: none;
   }
   }
 
 
   // display cheatsheet for comment form only
   // display cheatsheet for comment form only

+ 3 - 9
src/client/styles/scss/_draft.scss

@@ -1,14 +1,5 @@
 .draft-list-item {
 .draft-list-item {
   .panel-heading {
   .panel-heading {
-    .caret {
-      transition: 0.4s;
-      transform: rotate(-90deg);
-
-      &.caret-opened {
-        transform: rotate(0deg);
-      }
-    }
-
     .icon-container {
     .icon-container {
       a:hover {
       a:hover {
         text-decoration: unset;
         text-decoration: unset;
@@ -30,4 +21,7 @@
   .draft-copy {
   .draft-copy {
     cursor: pointer;
     cursor: pointer;
   }
   }
+  .draft-path {
+    cursor: pointer;
+  }
 }
 }

+ 1 - 1
src/client/styles/scss/_layout_growi.scss

@@ -27,7 +27,7 @@
     position: sticky;
     position: sticky;
     // growisubnavigation + grw-navbar-boder
     // growisubnavigation + grw-navbar-boder
     top: calc(100px + 4px);
     top: calc(100px + 4px);
-    min-width: 100%;
+    width: 250px;
     margin-top: 5px;
     margin-top: 5px;
 
 
     .revision-toc-content {
     .revision-toc-content {

+ 11 - 1
src/client/styles/scss/_page_list.scss

@@ -9,7 +9,7 @@ body .page-list {
     margin: 0;
     margin: 0;
 
 
     > li {
     > li {
-      margin: 0;
+      margin: 0.5rem;
       list-style: none;
       list-style: none;
 
 
       > a {
       > a {
@@ -72,3 +72,13 @@ body .page-list {
     background-color: $gray-300;
     background-color: $gray-300;
   }
   }
 }
 }
+
+.grw-page-list-m {
+  .grw-page-list-title-m {
+    svg {
+      width: 35px;
+      height: 35px;
+      margin-bottom: 6px;
+    }
+  }
+}

+ 0 - 4
src/client/styles/scss/_subnav.scss

@@ -47,10 +47,6 @@
   }
   }
 
 
   ul.authors {
   ul.authors {
-    padding: 0.7em 0 0.7em 1.5em;
-    margin-bottom: 0;
-    margin-left: 1em;
-
     li {
     li {
       font-size: 12px;
       font-size: 12px;
       list-style: none;
       list-style: none;

+ 14 - 2
src/client/styles/scss/_toc.scss

@@ -2,6 +2,8 @@
   flex-wrap: wrap;
   flex-wrap: wrap;
 
 
   .grw-btn-top-of-table {
   .grw-btn-top-of-table {
+    width: 35px;
+    height: 35px;
     svg {
     svg {
       width: 16px;
       width: 16px;
       height: 16px;
       height: 16px;
@@ -30,7 +32,9 @@
 
 
   .revision-toc-content {
   .revision-toc-content {
     padding: 10px;
     padding: 10px;
-
+    li {
+      margin: 6px;
+    }
     > ul {
     > ul {
       padding-left: 0;
       padding-left: 0;
       ul {
       ul {
@@ -41,7 +45,15 @@
     // first level of li
     // first level of li
     > ul > li {
     > ul > li {
       padding: 5px;
       padding: 5px;
-      margin: 4px 4px 4px 17px;
+      margin-right: 4px;
+      margin-left: 17px;
+    }
+  }
+
+  .grw-icon-container-recently-created {
+    svg {
+      width: 14px;
+      height: 14px;
     }
     }
   }
   }
 }
 }

+ 2 - 2
src/client/styles/scss/_user.scss

@@ -20,8 +20,8 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   }
   }
 
 
   .picture {
   .picture {
-    width: 72px;
-    height: 72px;
+    width: 120px;
+    height: 120px;
   }
   }
 
 
   div.user-page-meta {
   div.user-page-meta {

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

@@ -44,6 +44,12 @@ textarea.form-control {
   // border: 1px solid darken($border, 30%);
   // border: 1px solid darken($border, 30%);
 }
 }
 
 
+.grw-slack-notification {
+  .form-control {
+    background: $bgcolor-global;
+  }
+}
+
 .form-control[disabled],
 .form-control[disabled],
 .form-control[readonly] {
 .form-control[readonly] {
   color: lighten($color-global, 10%);
   color: lighten($color-global, 10%);
@@ -107,11 +113,6 @@ ul.pagination {
     button.page-link {
     button.page-link {
       @extend .btn-dark;
       @extend .btn-dark;
     }
     }
-    &.active {
-      button {
-        @extend .active;
-      }
-    }
   }
   }
 }
 }
 
 
@@ -348,6 +349,12 @@ body.on-edit {
   }
   }
 }
 }
 
 
+.growi .main {
+  .page-comments-row {
+    background: $bgcolor-subnav;
+  }
+}
+
 /*
 /*
  * GROWI tags
  * GROWI tags
  */
  */
@@ -357,3 +364,14 @@ body.on-edit {
     background-color: $bgcolor-tags;
     background-color: $bgcolor-tags;
   }
   }
 }
 }
+
+/*
+ * GROWI user page
+ */
+.grw-page-list-m {
+  .grw-page-list-title-m {
+    svg {
+      fill: $color-global;
+    }
+  }
+}

+ 23 - 0
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -36,6 +36,12 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: $bgcolor-global;
   background-color: $bgcolor-global;
 }
 }
 
 
+.grw-slack-notification {
+  .form-control {
+    background: white;
+  }
+}
+
 .form-control::placeholder {
 .form-control::placeholder {
   color: darken($bgcolor-global, 20%);
   color: darken($bgcolor-global, 20%);
 }
 }
@@ -273,6 +279,12 @@ $table-hover-bg: $bgcolor-table-hover;
   }
   }
 }
 }
 
 
+.growi .main {
+  .page-comments-row {
+    background: $bgcolor-subnav;
+  }
+}
+
 /*
 /*
  * GROWI tags
  * GROWI tags
  */
  */
@@ -282,3 +294,14 @@ $table-hover-bg: $bgcolor-table-hover;
     background-color: $bgcolor-tags;
     background-color: $bgcolor-tags;
   }
   }
 }
 }
+
+/*
+ * GROWI user page
+ */
+.grw-page-list-m {
+  .grw-page-list-title-m {
+    svg {
+      fill: $color-global;
+    }
+  }
+}

+ 43 - 7
src/client/styles/scss/theme/_apply-colors.scss

@@ -118,6 +118,36 @@ pre:not(.hljs):not(.CodeMirror-line) {
   }
   }
 }
 }
 
 
+// Pagination
+ul.pagination {
+  li.page-item.disabled {
+    button.page-link {
+      color: $gray-400;
+    }
+  }
+  li.page-item.active {
+    button.page-link {
+      color: color-yiq($primary);
+      background-color: $primary;
+      &:hover {
+        color: color-yiq($primary);
+        background-color: $primary;
+      }
+    }
+  }
+  li.page-item {
+    button.page-link {
+      color: $primary;
+      border-color: $secondary;
+      &:hover,
+      &:active,
+      &:focus {
+        color: $primary;
+      }
+    }
+  }
+}
+
 //
 //
 //== Apply to Handsontable
 //== Apply to Handsontable
 //
 //
@@ -419,19 +449,26 @@ body.on-edit {
 }
 }
 
 
 /*
 /*
- * GROWI comment form
+ * GROWI comment
  */
  */
-.growi .main {
-  .page-comments-row {
-    border-top-color: $border-color-theme;
+.page-comment-meta .page-comment-revision svg {
+  fill: $color-link;
+
+  &:hover() {
+    fill: $color-link-hover;
   }
   }
+}
 
 
+/*
+ * GROWI comment form
+ */
+.growi .main {
   .page-comment .page-comment-main,
   .page-comment .page-comment-main,
   .page-comment-form .comment-form-main {
   .page-comment-form .comment-form-main {
-    background-color: darken($bgcolor-global, 4%);
+    background-color: $bgcolor-global;
 
 
     &:before {
     &:before {
-      border-right-color: darken($bgcolor-global, 4%);
+      border-right-color: $bgcolor-global;
     }
     }
 
 
     .nav.nav-tabs {
     .nav.nav-tabs {
@@ -454,7 +491,6 @@ body.on-edit {
         > li.nav-item > a.nav-link {
         > li.nav-item > a.nav-link {
           color: inherit;
           color: inherit;
         }
         }
-
         a {
         a {
           &.hover {
           &.hover {
             background-color: darken($bgcolor-global, 4%);
             background-color: darken($bgcolor-global, 4%);

+ 62 - 0
src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss

@@ -1,5 +1,64 @@
 $theme-colors: map-merge($theme-colors, $colors);
 $theme-colors: map-merge($theme-colors, $colors);
 
 
+@mixin button-svg-icon-variant($background, $hover-background: darken($background, 7.5%), $active-background: darken($background, 10%)) {
+  svg {
+    fill: color-yiq($background);
+  }
+
+  @include hover() {
+    svg {
+      fill: color-yiq($hover-background);
+    }
+  }
+
+  &:focus,
+  &.focus {
+    svg {
+      fill: color-yiq($hover-background);
+    }
+  }
+
+  // Disabled comes first so active can properly restyle
+  &.disabled,
+  &:disabled {
+    svg {
+      color: color-yiq($background);
+    }
+  }
+
+  &:not(:disabled):not(.disabled):active,
+  &:not(:disabled):not(.disabled).active,
+  .show > &.dropdown-toggle {
+    svg {
+      color: color-yiq($active-background);
+    }
+  }
+}
+
+@mixin button-outline-svg-icon-variant($value, $color-hover: $value) {
+  svg {
+    fill: $value;
+  }
+  @include hover() {
+    svg {
+      fill: $value;
+    }
+  }
+  &.disabled,
+  &:disabled {
+    svg {
+      fill: $value;
+    }
+  }
+  &:not(:disabled):not(.disabled):active,
+  &:not(:disabled):not(.disabled).active,
+  .show > &.dropdown-toggle {
+    svg {
+      fill: $value;
+    }
+  }
+}
+
 @each $color, $value in $theme-colors {
 @each $color, $value in $theme-colors {
   @include bg-variant('.bg-#{$color}', $value);
   @include bg-variant('.bg-#{$color}', $value);
 }
 }
@@ -17,11 +76,14 @@ $theme-colors: map-merge($theme-colors, $colors);
 @each $color, $value in $theme-colors {
 @each $color, $value in $theme-colors {
   .btn-#{$color} {
   .btn-#{$color} {
     @include button-variant($value, $value);
     @include button-variant($value, $value);
+    @include button-svg-icon-variant($value, $value);
   }
   }
 }
 }
+
 @each $color, $value in $theme-colors {
 @each $color, $value in $theme-colors {
   .btn-outline-#{$color} {
   .btn-outline-#{$color} {
     @include button-outline-variant($value, $color-hover: $value, $active-background: rgba($value, 0.1), $active-border: $value);
     @include button-outline-variant($value, $color-hover: $value, $active-background: rgba($value, 0.1), $active-border: $value);
+    @include button-outline-svg-icon-variant($value, $color-hover: $value);
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled):active,
     &:not(:disabled):not(.disabled).active,
     &:not(:disabled):not(.disabled).active,
     .show > &.dropdown-toggle {
     .show > &.dropdown-toggle {

+ 1 - 2
src/server/routes/apiv3/app-settings.js

@@ -138,7 +138,6 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  const loginRequired = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const adminRequired = require('../../middlewares/admin-required')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
@@ -205,7 +204,7 @@ module.exports = (crowi) => {
    *                      type: object
    *                      type: object
    *                      description: app settings params
    *                      description: app settings params
    */
    */
-  router.get('/', accessTokenParser, loginRequired, adminRequired, async(req, res) => {
+  router.get('/', accessTokenParser, loginRequiredStrictly, adminRequired, async(req, res) => {
     const appSettingsParams = {
     const appSettingsParams = {
       title: crowi.configManager.getConfig('crowi', 'app:title'),
       title: crowi.configManager.getConfig('crowi', 'app:title'),
       confidential: crowi.configManager.getConfig('crowi', 'app:confidential'),
       confidential: crowi.configManager.getConfig('crowi', 'app:confidential'),

+ 4 - 3
src/server/routes/apiv3/page.js

@@ -112,7 +112,8 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  */
  */
 module.exports = (crowi) => {
 module.exports = (crowi) => {
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
   const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
-  const loginRequired = require('../../middlewares/login-required')(crowi);
+  const loginRequired = require('../../middlewares/login-required')(crowi, true);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
   const csrf = require('../../middlewares/csrf')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
   const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
 
@@ -165,7 +166,7 @@ module.exports = (crowi) => {
    *                schema:
    *                schema:
    *                  $ref: '#/components/schemas/Page'
    *                  $ref: '#/components/schemas/Page'
    */
    */
-  router.put('/likes', accessTokenParser, loginRequired, csrf, validator.likes, apiV3FormValidator, async(req, res) => {
+  router.put('/likes', accessTokenParser, loginRequiredStrictly, csrf, validator.likes, apiV3FormValidator, async(req, res) => {
     const { pageId, bool } = req.body;
     const { pageId, bool } = req.body;
 
 
     let page;
     let page;
@@ -227,7 +228,7 @@ module.exports = (crowi) => {
   *          200:
   *          200:
   *            description: Return page's markdown
   *            description: Return page's markdown
   */
   */
-  router.get('/export/:pageId', loginRequired, validator.export, async(req, res) => {
+  router.get('/export/:pageId', loginRequiredStrictly, validator.export, async(req, res) => {
     const { pageId } = req.params;
     const { pageId } = req.params;
     const { format, revisionId = null } = req.query;
     const { format, revisionId = null } = req.query;
     let revision;
     let revision;

+ 2 - 0
src/server/routes/index.js

@@ -116,6 +116,8 @@ module.exports = function(crowi, app) {
   app.get('/me'                       , loginRequiredStrictly , me.index);
   app.get('/me'                       , loginRequiredStrictly , me.index);
   // external-accounts
   // external-accounts
   app.get('/me/external-accounts'                         , loginRequiredStrictly , me.externalAccounts.list);
   app.get('/me/external-accounts'                         , loginRequiredStrictly , me.externalAccounts.list);
+  // my drafts
+  app.get('/me/drafts'                , loginRequiredStrictly, me.drafts.list);
 
 
   app.get('/:id([0-9a-z]{24})'       , loginRequired , page.redirector);
   app.get('/:id([0-9a-z]{24})'       , loginRequired , page.redirector);
   app.get('/_r/:id([0-9a-z]{24})'    , loginRequired , page.redirector); // alias
   app.get('/_r/:id([0-9a-z]{24})'    , loginRequired , page.redirector); // alias

+ 5 - 0
src/server/routes/me.js

@@ -128,6 +128,11 @@ module.exports = function(crowi, app) {
       });
       });
   };
   };
 
 
+  actions.drafts = {};
+  actions.drafts.list = async function(req, res) {
+    return res.render('me/drafts');
+  };
+
   actions.updates = function(req, res) {
   actions.updates = function(req, res) {
     res.render('me/update', {
     res.render('me/update', {
     });
     });

+ 0 - 1
src/server/views/layout-growi/forbidden.html

@@ -5,7 +5,6 @@
   {% include '../widget/page_alerts.html' %}
   {% include '../widget/page_alerts.html' %}
 {% endblock %}
 {% endblock %}
 
 
-
 {% block content_main %}
 {% block content_main %}
   <div class="row">
   <div class="row">
     <div class="col grw-page-content-container">
     <div class="col grw-page-content-container">

+ 6 - 6
src/server/views/layout-growi/page.html

@@ -6,18 +6,18 @@
 
 
 
 
 {% block content_main %}
 {% block content_main %}
-  <div class="row">
+  <div class="d-flex justify-content-between">
 
 
-    <div class="col grw-page-content-container">
+    <div class="grw-page-content-container flex-grow-1">
 
 
       {% include '../widget/page_content.html' %}
       {% include '../widget/page_content.html' %}
 
 
     </div>
     </div>
 
 
-    <div class="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
-        <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
-          <div id="revision-toc-content" class="revision-toc-content"></div>
-        </div>
+    <div class="d-none d-lg-block revision-toc-container">
+      <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
+        <div id="revision-toc-content" class="revision-toc-content"></div>
+      </div>
     </div>
     </div>
 
 
   </div>
   </div>

+ 3 - 3
src/server/views/layout-growi/page_list.html

@@ -6,15 +6,15 @@
 
 
 
 
 {% block content_main %}
 {% block content_main %}
-  <div class="row">
+  <div class="d-flex justify-content-between">
 
 
-    <div class="col grw-page-content-container">
+    <div class="grw-page-content-container flex-grow-1">
 
 
       {% include '../widget/page_content.html' %}
       {% include '../widget/page_content.html' %}
 
 
     </div>
     </div>
 
 
-    <div class="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
+    <div class="d-none d-lg-block revision-toc-container">
       <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
       <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
         <div id="revision-toc-content" class="revision-toc-content"></div>
         <div id="revision-toc-content" class="revision-toc-content"></div>
       </div>
       </div>

+ 3 - 3
src/server/views/layout-growi/shared_page.html

@@ -15,13 +15,13 @@
 
 
 {% block content_main %}
 {% block content_main %}
   <div
   <div
-    class="row"
+    class="d-flex justify-content-between"
     id="is-shared-page"
     id="is-shared-page"
     data-share-link-expired-at="{% if sharelink.expiredAt %}{{ sharelink.expiredAt|datetz('Y/m/d H:i:s')}}{% endif %}"
     data-share-link-expired-at="{% if sharelink.expiredAt %}{{ sharelink.expiredAt|datetz('Y/m/d H:i:s')}}{% endif %}"
     data-share-link-created-at="{{ sharelink.createdAt|datetz('Y/m/d H:i:s')}}"
     data-share-link-created-at="{{ sharelink.createdAt|datetz('Y/m/d H:i:s')}}"
   >
   >
     {% block content_page %}
     {% block content_page %}
-      <div class="col grw-page-content-container">
+      <div class="grw-page-content-container flex-grow-1">
         <div id="share-link-alert"></div>
         <div id="share-link-alert"></div>
 
 
         {% include '../widget/page_content.html' %}
         {% include '../widget/page_content.html' %}
@@ -33,7 +33,7 @@
       </div>
       </div>
 
 
       {# relocate #revision-toc #}
       {# relocate #revision-toc #}
-      <div class="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
+      <div class="d-none d-lg-block revision-toc-container">
         <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
         <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="123">
           <div id="revision-toc-content" class="revision-toc-content"></div>
           <div id="revision-toc-content" class="revision-toc-content"></div>
         </div>
         </div>

+ 30 - 6
src/server/views/layout-growi/user_page.html

@@ -6,18 +6,18 @@
 {% endblock %}
 {% endblock %}
 
 
 {% block content_main %}
 {% block content_main %}
-  <div class="row">
 
 
-    <div class="col grw-page-content-container">
+  <div class="d-flex justify-content-between">
+    <div class="grw-page-content-container flex-grow-1">
+
+      <div class="user-info" id="user-info">
+      </div>
 
 
       {#
       {#
-        # ensure to insert 'user_page_content' widget to here
-        #
         #   Because this block has content like 'Bookmarks' or 'Recent Created' whose height changes dynamically,
         #   Because this block has content like 'Bookmarks' or 'Recent Created' whose height changes dynamically,
         #   setting of 'revision-toc' (affix) is hindered.
         #   setting of 'revision-toc' (affix) is hindered.
         #}
         #}
       <div class="mb-5 user-page-content-container d-edit-none d-print-none">
       <div class="mb-5 user-page-content-container d-edit-none d-print-none">
-        {% include '../widget/user_page_content.html' %}
       </div>
       </div>
 
 
       {% block content_main_before %}
       {% block content_main_before %}
@@ -34,7 +34,7 @@
     </div> {# /.col- #}
     </div> {# /.col- #}
 
 
     {# relocate #revision-toc #}
     {# relocate #revision-toc #}
-    <div class="col-xl-2 col-lg-3 d-none d-lg-block revision-toc-container">
+    <div class="d-none d-lg-block revision-toc-container">
       <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="116">
       <div id="revision-toc" class="revision-toc mt-3 sps sps--abv" data-sps-offset="116">
         <div id="revision-toc-content" class="revision-toc-content"></div>
         <div id="revision-toc-content" class="revision-toc-content"></div>
       </div>
       </div>
@@ -49,6 +49,30 @@
 {% block content_main_after %}
 {% block content_main_after %}
   {% include 'widget/comments.html' %}
   {% include 'widget/comments.html' %}
 
 
+  {% if page %}
+    <div class="grw-page-list-m mt-5 pb-5 d-edit-none">
+      <h2 class="grw-page-list-title-m border-bottom pb-2 mb-3" id="bookmarks-list">
+        <i id="user-bookmark-icon"></i>
+        Bookmarks
+      </h2>
+      <div class="page-list" id="user-bookmark-list">
+        <div class="page-list-container">
+        </div>
+      </div>
+    </div>
+
+    <div class="grw-page-list-m mt-5 pb-5 d-edit-none">
+      <h2 class="grw-page-list-title-m border-bottom pb-2 mb-3" id="recently-created-list">
+        <i id="recent-created-icon"></i>
+        Recently Created
+      </h2>
+      <div class="page-list" id="user-created-list">
+        <div class="page-list-container">
+        </div>
+      </div>
+    </div>
+  {% endif %}
+
   {% if page %}
   {% if page %}
     {% include '../widget/page_attachments.html' %}
     {% include '../widget/page_attachments.html' %}
   {% endif %}
   {% endif %}

+ 2 - 2
src/server/views/layout-growi/widget/comments.html

@@ -1,8 +1,8 @@
 <div class="page-comments-row row d-edit-none d-print-none">
 <div class="page-comments-row row d-edit-none d-print-none">
 
 
-  <div class="page-comments col-xl-7 col-lg-9">
+  <div class="page-comments col-lg-10 my-5">
 
 
-    <h4 class="my-2"><i class="icon-fw icon-bubbles"></i> Comments</h4>
+    <h2 class="border-bottom pb-2 mb-3"><i class="icon-fw icon-bubbles"></i> Comments</h2>
 
 
     <div class="page-comments-list" id="page-comments-list"></div>
     <div class="page-comments-list" id="page-comments-list"></div>
 
 

+ 18 - 0
src/server/views/me/drafts.html

@@ -0,0 +1,18 @@
+{% extends '../layout-growi/base/layout.html' %}
+
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('My Drafts')) }}{% endblock %}
+
+{% block content_header %}
+{% endblock %}
+
+{% block content_main %}
+<div id="content-main" class="content-main container">
+  <div id="my-drafts"></div>
+</div>
+{% endblock content_main %}
+
+{% block content_footer %}
+{% endblock content_footer %}
+
+{% block layout_footer %}
+{% endblock layout_footer %}

+ 1 - 1
src/server/views/widget/alert_siteurl_undefined.html

@@ -1,5 +1,5 @@
 {% if !getConfig('crowi', 'app:siteUrl') %}
 {% if !getConfig('crowi', 'app:siteUrl') %}
-<div class="alert alert-danger mb-0 px-4 py-2">
+<div class="alert alert-danger d-edit-none mb-0 px-4 py-2">
   <i class="icon-exclamation"></i>
   <i class="icon-exclamation"></i>
   {{ t("security_setting.alert_siteUrl_is_not_set", { link:t('App Settings')}) }}
   {{ t("security_setting.alert_siteUrl_is_not_set", { link:t('App Settings')}) }}
 </div>
 </div>

+ 2 - 0
src/server/views/widget/forbidden_content.html

@@ -10,6 +10,8 @@
 <div id="content-main" class="content-main page-list"
 <div id="content-main" class="content-main page-list"
   data-path="{{ encodeURI(path) }}"
   data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
+  data-page-is-forbidden="true"
+  data-page-is-not-creatable="true"
   >
   >
 
 
   <div class="row row-alerts d-edit-none">
   <div class="row row-alerts d-edit-none">

+ 1 - 1
src/server/views/widget/not_creatable_content.html

@@ -10,7 +10,7 @@
 <div id="content-main" class="content-main page-list"
 <div id="content-main" class="content-main page-list"
   data-path="{{ encodeURI(path) }}"
   data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
-  data-page-is-creatable="true"
+  data-page-is-not-creatable="true"
   ></div>
   ></div>
 
 
 </div>
 </div>

+ 2 - 2
src/server/views/widget/page_content.html

@@ -14,10 +14,10 @@
   data-page-grant-group-name="{{ grantedGroupName }}"
   data-page-grant-group-name="{{ grantedGroupName }}"
   data-page-is-liked="{% if user %}{{ page.isLiked(user) }}{% else %}false{% endif %}"
   data-page-is-liked="{% if user %}{{ page.isLiked(user) }}{% else %}false{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
   data-page-is-seen="{% if page and page.isSeenUser(user) %}1{% else %}0{% endif %}"
-  data-page-is-forbidden="{% if forbidden %}true{% else %}false{% endif %}"
+  data-page-is-forbidden="false"
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
   data-page-is-deleted="{% if page.isDeleted() %}true{% else %}false{% endif %}"
   data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"
   data-page-is-deletable="{% if isDeletablePage() %}true{% else %}false{% endif %}"
-  data-page-is-creatable="false"
+  data-page-is-not-creatable="false"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-page-is-able-to-delete-completely="{% if user.canDeleteCompletely(page.creator._id) %}true{% else %}false{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-slack-channels="{{ slack|default('') }}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"
   data-page-created-at="{% if page %}{{ page.createdAt|datetz('Y/m/d H:i:s') }}{% endif %}"

+ 0 - 65
src/server/views/widget/user_page_content.html

@@ -1,65 +0,0 @@
-<div class="user-page-content mb-4">
-  <ul class="nav nav-tabs user-page-content-menu mb-4" role="tablist">
-    <li class="nav-item">
-      <a class="nav-link active" href="#user-bookmark-list" role="tab" data-toggle="tab">
-        <i class="icon-star"></i>
-        <span class="d-none d-sm-inline">Bookmarks</span>
-      </a>
-    </li>
-    <li class="nav-item">
-      <a class="nav-link" href="#user-created-list" role="tab" data-toggle="tab">
-        <i class="icon-clock"></i>
-        <span class="d-none d-sm-inline">Recently Created</span>
-      </a>
-    </li>
-
-    {% if user._id.toString() == pageUser._id.toString() %}
-    <li class="nav-item">
-      <a class="nav-link" href="/me" role="tab">
-        <i class="icon-wrench"></i>
-        <span class="d-none d-sm-inline">Settings</span>
-      </a>
-    </li>
-    {% endif %}
-  </ul>
-
-  <div class="user-page-content-tab tab-content">
-
-    <div class="tab-pane user-bookmark-list page-list active" id="user-bookmark-list">
-      {% if bookmarkList.length == 0 %}
-        {{t('No bookmarks yet')}}.
-      {% else %}
-        <div class="page-list-container">
-          {# {% include 'page_list.html' with { pages: bookmarkList, pagePropertyName: 'page' } %} #}
-        </div>
-      {% endif %}
-    </div>
-
-    <div class="tab-pane user-created-list page-list" id="user-created-list">
-      <div class="page-list-container">
-      </div>
-    </div>
-
-    {% if user._id.toString() == pageUser._id.toString() %}
-    <div class="tab-pane user-draft-list page-list" id="user-draft-list">
-      <div class="page-list-container">
-      </div>
-    </div>
-    {% endif %}
-  </div>
-</div>
-
-<script>
-  function activateTab(tab){
-    $('.nav-tabs a[href="#' + tab + '"]').tab('show');
-  };
-
-  window.addEventListener('load', function(e) {
-    // hash on page
-    if (location.hash) {
-      if (location.hash == '#user-draft-list') {
-        activateTab('user-draft-list');
-      }
-    }
-  });
-</script>