Explorar el Código

Merge branch 'master' into feat/gw3604-userhome-renovation

# Conflicts:
#	src/client/styles/scss/_layout_kibela.scss
#	src/server/views/widget/page_tabs.html
yusuketk hace 5 años
padre
commit
e969f9f30a
Se han modificado 83 ficheros con 918 adiciones y 1763 borrados
  1. 1 1
      .devcontainer/docker-compose.yml
  2. 0 35
      .github/workflows/prerelease.yml
  3. 0 12
      .github/workflows/release.yml
  4. 18 2
      CHANGES.md
  5. 1 1
      package.json
  6. BIN
      public/images/admin/customize/layout-classic-thumb.gif
  7. BIN
      public/images/admin/customize/layout-classic.gif
  8. BIN
      public/images/admin/customize/layout-crowi-plus-thumb.gif
  9. BIN
      public/images/admin/customize/layout-crowi-plus.gif
  10. BIN
      public/images/admin/customize/layout-kibela-thumb.gif
  11. BIN
      public/images/admin/customize/layout-kibela.gif
  12. 0 12
      resource/locales/en_US/admin/admin.json
  13. 4 0
      resource/locales/en_US/translation.json
  14. 0 12
      resource/locales/ja_JP/admin/admin.json
  15. 4 0
      resource/locales/ja_JP/translation.json
  16. 0 16
      resource/locales/zh_CN/admin/admin.json
  17. 6 2
      resource/locales/zh_CN/translation.json
  18. 3 17
      src/client/js/app.jsx
  19. 0 2
      src/client/js/base.jsx
  20. 1 1
      src/client/js/components/Admin/Customize/Customize.jsx
  21. 0 48
      src/client/js/components/Admin/Customize/CustomizeLayoutOption.jsx
  22. 0 67
      src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx
  23. 2 0
      src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx
  24. 6 13
      src/client/js/components/Admin/Customize/CustomizeThemeSetting.jsx
  25. 9 8
      src/client/js/components/Admin/ManageExternalAccount.jsx
  26. 33 28
      src/client/js/components/Admin/Security/ShareLinkSetting.jsx
  27. 2 1
      src/client/js/components/Admin/UserManagement.jsx
  28. 21 11
      src/client/js/components/Fab.jsx
  29. 16 0
      src/client/js/components/Icons/PagePreviewIcon.jsx
  30. 22 0
      src/client/js/components/Icons/PresentationIcon.jsx
  31. 4 8
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  32. 39 0
      src/client/js/components/Navbar/ThreeStrandedButton.jsx
  33. 18 2
      src/client/js/components/Page/PageManagement.jsx
  34. 91 62
      src/client/js/components/PageAccessoriesModal.jsx
  35. 16 4
      src/client/js/components/PageAttachment.jsx
  36. 1 1
      src/client/js/components/PageEditor/CodeMirrorEditor.jsx
  37. 270 150
      src/client/js/components/PageEditor/LinkEditModal.jsx
  38. 3 4
      src/client/js/components/PageEditor/MarkdownLinkUtil.js
  39. 8 10
      src/client/js/components/PageHistory.jsx
  40. 3 2
      src/client/js/components/PageList.jsx
  41. 31 0
      src/client/js/components/PagePresentationModal.jsx
  42. 22 20
      src/client/js/components/PageTimeline.jsx
  43. 19 1
      src/client/js/components/PaginationWrapper.jsx
  44. 3 6
      src/client/js/components/ShareLink/ShareLink.jsx
  45. 2 2
      src/client/js/components/TopOfTableContents.jsx
  46. 0 23
      src/client/js/legacy/crowi.js
  47. 7 26
      src/client/js/models/Linker.js
  48. 4 19
      src/client/js/services/AdminCustomizeContainer.js
  49. 1 0
      src/client/js/services/PageAccessoriesContainer.js
  50. 0 169
      src/client/styles/scss/_comment_kibela.scss
  51. 0 170
      src/client/styles/scss/_layout_kibela.scss
  52. 13 5
      src/client/styles/scss/_linkedit-preview.scss
  53. 0 36
      src/client/styles/scss/_navbar_kibela.scss
  54. 13 0
      src/client/styles/scss/_page-presentation.scss
  55. 0 45
      src/client/styles/scss/_page.scss
  56. 35 2
      src/client/styles/scss/_page_accessaries_modal.scss
  57. 1 4
      src/client/styles/scss/style-app.scss
  58. 40 1
      src/client/styles/scss/theme/_apply-colors-dark.scss
  59. 0 186
      src/client/styles/scss/theme/_apply-colors-kibela.scss
  60. 26 0
      src/client/styles/scss/theme/_apply-colors-light.scss
  61. 16 2
      src/client/styles/scss/theme/_apply-colors.scss
  62. 30 1
      src/client/styles/scss/theme/kibela.scss
  63. 25 3
      src/server/routes/apiv3/attachment.js
  64. 21 26
      src/server/routes/apiv3/customize-setting.js
  65. 0 1
      src/server/routes/apiv3/pages.js
  66. 0 49
      src/server/views/layout-kibela/base/layout.html
  67. 0 13
      src/server/views/layout-kibela/expired_shared_page.html
  68. 0 19
      src/server/views/layout-kibela/forbidden.html
  69. 0 19
      src/server/views/layout-kibela/not_creatable.html
  70. 0 24
      src/server/views/layout-kibela/not_found.html
  71. 0 13
      src/server/views/layout-kibela/not_found_shared_page.html
  72. 0 40
      src/server/views/layout-kibela/page.html
  73. 0 43
      src/server/views/layout-kibela/page_list.html
  74. 0 46
      src/server/views/layout-kibela/shared_page.html
  75. 0 55
      src/server/views/layout-kibela/user_page.html
  76. 0 15
      src/server/views/layout-kibela/widget/comments.html
  77. 5 10
      src/server/views/layout/layout.html
  78. 0 5
      src/server/views/widget/headers/styles-theme-kibela.html
  79. 1 5
      src/server/views/widget/page_content.html
  80. 0 38
      src/server/views/widget/page_list_and_timeline_kibela.html
  81. 0 19
      src/server/views/widget/page_tabs.html
  82. 0 69
      src/server/views/widget/page_tabs_kibela.html
  83. 1 1
      src/server/views/widget/user_page_content.html

+ 1 - 1
.devcontainer/docker-compose.yml

@@ -37,7 +37,7 @@ services:
       - hackmd
       - hackmd
 
 
   mongo:
   mongo:
-    image: mongo:3.6
+    image: mongo:4.4
     restart: unless-stopped
     restart: unless-stopped
     ports:
     ports:
       - 27017:27017
       - 27017:27017

+ 0 - 35
.github/workflows/prerelease.yml

@@ -1,35 +0,0 @@
-name: Pre Release
-
-on:
-  push:
-    branches:
-      - master
-
-jobs:
-  build-image-for-cache:
-
-    runs-on: ubuntu-latest
-
-    steps:
-    - uses: actions/checkout@v2
-
-    - name: Set up Docker Buildx
-      uses: crazy-max/ghaction-docker-buildx@v3
-
-    - name: Cache Docker layers
-      uses: actions/cache@v2
-      id: cache
-      with:
-        path: /tmp/.buildx-cache
-        key: ${{ runner.OS }}-buildx-${{ hashFiles('**/yarn.lock') }}-default
-        restore-keys: |
-          ${{ runner.os }}-buildx-
-
-    - name: Build Docker Image
-      run: |
-        docker buildx build \
-          --cache-from "type=local,src=/tmp/.buildx-cache" \
-          --cache-to "type=local,dest=/tmp/.buildx-cache" \
-          --platform linux/amd64 \
-          --load \
-          --file ./docker/Dockerfile .

+ 0 - 12
.github/workflows/release.yml

@@ -78,21 +78,9 @@ jobs:
       run: |
       run: |
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
         echo ${{ secrets. DOCKER_REGISTRY_PASSWORD }} | docker login --username wsmoogle --password-stdin
 
 
-    - name: Cache Docker layers
-      uses: actions/cache@v2
-      id: cache
-      with:
-        path: /tmp/.buildx-cache
-        key: ${{ runner.OS }}-buildx-${{ hashFiles('**/yarn.lock') }}-${{ matrix.flavor }}
-        restore-keys: |
-          ${{ runner.OS }}-buildx-${{ hashFiles('**/yarn.lock') }}-
-          ${{ runner.OS }}-buildx-
-
     - name: Build Docker Image
     - name: Build Docker Image
       run: |
       run: |
         docker buildx build \
         docker buildx build \
-          --cache-from "type=local,src=/tmp/.buildx-cache" \
-          --cache-to "type=local,dest=/tmp/.buildx-cache" \
           --tag growi${{ env.SUFFIX }} \
           --tag growi${{ env.SUFFIX }} \
           --build-arg flavor=${{ matrix.flavor }} \
           --build-arg flavor=${{ matrix.flavor }} \
           --platform linux/amd64 \
           --platform linux/amd64 \

+ 18 - 2
CHANGES.md

@@ -1,8 +1,24 @@
 # CHANGES
 # CHANGES
 
 
-## v4.1.6-RC
+## v4.2.0
 
 
-* 
+### BREAKING CHANGES
+
+* GROWI v4.2.x no longer support Kibela layout
+    * Kibela theme is newly added and the configuration will migrate to it automatically
+
+### Updates
+
+* Improvement: Basic layout of page
+* Support: Support MongoDB 4.0, 4.2 and 4.4
+
+
+## v4.1.6
+
+* Improvement: Hide Fab at admin pages
+* Fix: Presentation does not work
+* Fix: Update GrantSelector status when uploading a file to a new page
+* Fix: CopyDropdown origin refs draw.io host wrongly
 
 
 ## v4.1.5
 ## v4.1.5
 
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.1.6-RC",
+  "version": "4.2.0-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

BIN
public/images/admin/customize/layout-classic-thumb.gif


BIN
public/images/admin/customize/layout-classic.gif


BIN
public/images/admin/customize/layout-crowi-plus-thumb.gif


BIN
public/images/admin/customize/layout-crowi-plus.gif


BIN
public/images/admin/customize/layout-kibela-thumb.gif


BIN
public/images/admin/customize/layout-kibela.gif


+ 0 - 12
resource/locales/en_US/admin/admin.json

@@ -90,19 +90,7 @@
     }
     }
   },
   },
   "customize_setting": {
   "customize_setting": {
-    "recommended": "Recommended",
-    "layout": "Layout",
     "theme": "Theme",
     "theme": "Theme",
-    "layout_desc": {
-      "growi_title": "Simple and clear",
-      "growi_text1": "Full screen layout and thin margins/paddings",
-      "growi_text2": "Show and post comments at the bottom of the page",
-      "growi_text3": "Affix table-of-contents",
-      "kibela_title": "Easy viewing structure",
-      "kibela_text1": "Center aligned contents",
-      "kibela_text2": "Show and post comments at the bottom of the page",
-      "kibela_text3": "Affix Table-of-contents"
-    },
     "theme_desc": {
     "theme_desc": {
       "light_and_dark": "Light and dark modes",
       "light_and_dark": "Light and dark modes",
       "unique": "Only one mode"
       "unique": "Only one mode"

+ 4 - 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",
@@ -47,6 +48,7 @@
   "Timeline View": "Timeline",
   "Timeline View": "Timeline",
   "History": "History",
   "History": "History",
   "attachment_data": "Attachment Data",
   "attachment_data": "Attachment Data",
+  "No_attachments_yet": "No attachments yet.",
   "Presentation Mode": "Presentation",
   "Presentation Mode": "Presentation",
   "Not available for guest": "Not available for guest",
   "Not available for guest": "Not available for guest",
   "Create Archive Page": "Create Archive Page",
   "Create Archive Page": "Create Archive Page",
@@ -138,6 +140,7 @@
   "Deleted Pages": "Deleted Pages",
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
   "Sign out": "Logout",
   "Disassociate": "Disassociate",
   "Disassociate": "Disassociate",
+  "No bookmarks yet": "No bookmarks yet",
   "Recent Created": "Recent Created",
   "Recent Created": "Recent Created",
   "Recent Changes": "Recent Changes",
   "Recent Changes": "Recent Changes",
   "personal_dropdown": {
   "personal_dropdown": {
@@ -430,6 +433,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.",
     "start_to_edit": "Start to edit with HackMD",
     "start_to_edit": "Start to edit with HackMD",
     "clone_page_content": "Click to clone page content and start to edit.",
     "clone_page_content": "Click to clone page content and start to edit.",

+ 0 - 12
resource/locales/ja_JP/admin/admin.json

@@ -90,19 +90,7 @@
     }
     }
   },
   },
   "customize_setting": {
   "customize_setting": {
-    "recommended": "おすすめ",
-    "layout": "レイアウト",
     "theme": "テーマ",
     "theme": "テーマ",
-    "layout_desc": {
-      "growi_title": "シンプル・明瞭",
-      "growi_text1": "全画面レイアウトで、余白は少なくなります。",
-      "growi_text2": "コメントはページの下部に表示されます。",
-      "growi_text3": "ページ情報は下部に表示されます。",
-      "kibela_title": "閲覧重視の構造",
-      "kibela_text1": "コンテンツが中心に表示されます。",
-      "kibela_text2": "コメントはページの下部に表示されます。",
-      "kibela_text3": "ページ情報は下部に表示されます。"
-    },
     "theme_desc" : {
     "theme_desc" : {
       "light_and_dark": "Light/Dark モード選択あり",
       "light_and_dark": "Light/Dark モード選択あり",
       "unique": "モード選択なし"
       "unique": "モード選択なし"

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

@@ -1,5 +1,6 @@
 {
 {
   "Help": "ヘルプ",
   "Help": "ヘルプ",
+  "view": "View",
   "Edit": "編集",
   "Edit": "編集",
   "Delete": "削除",
   "Delete": "削除",
   "delete_all": "全て削除",
   "delete_all": "全て削除",
@@ -48,6 +49,7 @@
   "Timeline View": "タイムライン",
   "Timeline View": "タイムライン",
   "History": "更新履歴",
   "History": "更新履歴",
   "attachment_data": "添付データ",
   "attachment_data": "添付データ",
+  "No_attachments_yet": "No attachments yet.",
   "Presentation Mode": "プレゼンテーション",
   "Presentation Mode": "プレゼンテーション",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Not available for guest": "ゲストユーザーは利用できません",
   "Create Archive Page": "アーカイブページの作成",
   "Create Archive Page": "アーカイブページの作成",
@@ -141,6 +143,7 @@
   "Color mode": "カラーモード",
   "Color mode": "カラーモード",
   "Sidebar mode": "サイドバーモード",
   "Sidebar mode": "サイドバーモード",
   "Sidebar mode on Editor": "サイドバーモード(編集時)",
   "Sidebar mode on Editor": "サイドバーモード(編集時)",
+  "No bookmarks yet": "No bookmarks yet",
   "Recent Created": "最新の作成",
   "Recent Created": "最新の作成",
   "Recent Changes": "最新の変更",
   "Recent Changes": "最新の変更",
   "personal_dropdown": {
   "personal_dropdown": {
@@ -432,6 +435,7 @@
     "open_sandbox": "Sandbox を開く"
     "open_sandbox": "Sandbox を開く"
   },
   },
   "hackmd":{
   "hackmd":{
+    "hack_md": "HackMD",
     "not_set_up": "HackMD はセットアップされていません",
     "not_set_up": "HackMD はセットアップされていません",
     "start_to_edit": "HackMD を開始する",
     "start_to_edit": "HackMD を開始する",
     "clone_page_content": "ページを複製して編集を開始します",
     "clone_page_content": "ページを複製して編集を開始します",

+ 0 - 16
resource/locales/zh_CN/admin/admin.json

@@ -91,23 +91,7 @@
 		}
 		}
 	},
 	},
 	"customize_setting": {
 	"customize_setting": {
-		"recommended": "推荐",
-		"layout": "布局",
 		"theme": "主体",
 		"theme": "主体",
-		"layout_desc": {
-			"growi_title": "简约",
-			"growi_text1": "全屏布局 窄边距/填充",
-			"growi_text2": "页面底部显示和发布评论",
-			"growi_text3": "附目录",
-			"kibela_title": "清晰",
-			"kibela_text1": "内容居中对齐",
-			"kibela_text2": "在页面底部显示和发布评论",
-			"kibela_text3": "附目录",
-			"crowi_title": "分栏",
-			"crowi_text1": "可折叠边栏",
-			"crowi_text2": "在侧边栏中显示和发布评论",
-			"crowi_text3": "可折叠目录"
-		},
 		"behavior": "行为",
 		"behavior": "行为",
 		"behavior_desc": {
 		"behavior_desc": {
 			"growi_text1": "<code>/page</code> and <code>/page/</code> 都显示同一页。",
 			"growi_text1": "<code>/page</code> and <code>/page/</code> 都显示同一页。",

+ 6 - 2
resource/locales/zh_CN/translation.json

@@ -1,5 +1,6 @@
 {
 {
-	"Help": "帮助",
+  "Help": "帮助",
+  "view": "View",
 	"Edit": "编辑",
 	"Edit": "编辑",
 	"Delete": "删除",
 	"Delete": "删除",
 	"delete_all": "删除所有",
 	"delete_all": "删除所有",
@@ -49,6 +50,7 @@
 	"Timeline View": "时间线",
 	"Timeline View": "时间线",
   "History": "历史",
   "History": "历史",
   "attachment_data": "Attachment Data",
   "attachment_data": "Attachment Data",
+  "No_attachments_yet": "暂无附件",
 	"Presentation Mode": "演示文稿",
 	"Presentation Mode": "演示文稿",
   "Not available for guest": "Not available for guest",
   "Not available for guest": "Not available for guest",
   "Create Archive Page": "创建归档页",
   "Create Archive Page": "创建归档页",
@@ -145,7 +147,8 @@
 	"List Drafts": "草稿",
 	"List Drafts": "草稿",
 	"Deleted Pages": "已删除页",
 	"Deleted Pages": "已删除页",
 	"Sign out": "退出",
 	"Sign out": "退出",
-	"Disassociate": "解除关联",
+  "Disassociate": "解除关联",
+  "No bookmarks yet": "暂无书签",
 	"Recent Created": "最新创建",
 	"Recent Created": "最新创建",
 	"Recent Changes": "最新修改",
 	"Recent Changes": "最新修改",
 	"form_validation": {
 	"form_validation": {
@@ -406,6 +409,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.",
 		"start_to_edit": "Start to edit with HackMD",
 		"start_to_edit": "Start to edit with HackMD",
 		"clone_page_content": "Click to clone page content and start to edit.",
 		"clone_page_content": "Click to clone page content and start to edit.",

+ 3 - 17
src/client/js/app.jsx

@@ -14,13 +14,11 @@ import EditorNavbarBottom from './components/PageEditor/EditorNavbarBottom';
 import { defaultEditorOptions, defaultPreviewOptions } from './components/PageEditor/OptionsSelector';
 import { defaultEditorOptions, defaultPreviewOptions } from './components/PageEditor/OptionsSelector';
 import PageEditorByHackmd from './components/PageEditorByHackmd';
 import PageEditorByHackmd from './components/PageEditorByHackmd';
 import Page from './components/Page';
 import Page from './components/Page';
-import PageHistory from './components/PageHistory';
 import PageComments from './components/PageComments';
 import PageComments from './components/PageComments';
 import PageTimeline from './components/PageTimeline';
 import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
 import PageManagement from './components/Page/PageManagement';
 import PageManagement from './components/Page/PageManagement';
 import TrashPageAlert from './components/Page/TrashPageAlert';
 import TrashPageAlert from './components/Page/TrashPageAlert';
-import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import PageStatusAlert from './components/PageStatusAlert';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import RecentCreated from './components/RecentCreated/RecentCreated';
 import RecentlyCreatedIcon from './components/Icons/RecentlyCreatedIcon';
 import RecentlyCreatedIcon from './components/Icons/RecentlyCreatedIcon';
@@ -28,6 +26,7 @@ import MyDraftList from './components/MyDraftList/MyDraftList';
 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 Fab from './components/Fab';
 
 
 import PersonalSettings from './components/Me/PersonalSettings';
 import PersonalSettings from './components/Me/PersonalSettings';
 import NavigationContainer from './services/NavigationContainer';
 import NavigationContainer from './services/NavigationContainer';
@@ -81,6 +80,8 @@ Object.assign(componentMappings, {
   'page-timeline': <PageTimeline />,
   'page-timeline': <PageTimeline />,
 
 
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
+
+  'grw-fab-container': <Fab />,
 });
 });
 
 
 // additional definitions if data exists
 // additional definitions if data exists
@@ -88,9 +89,7 @@ if (pageContainer.state.pageId != null) {
   Object.assign(componentMappings, {
   Object.assign(componentMappings, {
     'page-comments-list': <PageComments />,
     'page-comments-list': <PageComments />,
     'page-comment-write': <CommentEditorLazyRenderer />,
     'page-comment-write': <CommentEditorLazyRenderer />,
-    'page-attachment': <PageAttachment />,
     'page-management': <PageManagement />,
     'page-management': <PageManagement />,
-
     'revision-toc': <TableOfContents />,
     'revision-toc': <TableOfContents />,
     'seen-user-list': <SeenUserList />,
     'seen-user-list': <SeenUserList />,
     'liker-list': <LikerList />,
     'liker-list': <LikerList />,
@@ -138,18 +137,5 @@ Object.keys(componentMappings).forEach((key) => {
   }
   }
 });
 });
 
 
-// うわーもうー (commented by Crowi team -- 2018.03.23 Yuki Takei)
-$('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <ErrorBoundary>
-        <Provider inject={injectableContainers}>
-          <PageHistory />
-        </Provider>
-      </ErrorBoundary>
-    </I18nextProvider>, document.getElementById('revision-history'),
-  );
-});
-
 // initialize scrollpos-styler
 // initialize scrollpos-styler
 ScrollPosStyler.init();
 ScrollPosStyler.init();

+ 0 - 2
src/client/js/base.jsx

@@ -8,7 +8,6 @@ import GrowiNavbarBottom from './components/Navbar/GrowiNavbarBottom';
 import Sidebar from './components/Sidebar';
 import Sidebar from './components/Sidebar';
 import ShareLinkAlert from './components/Page/ShareLinkAlert';
 import ShareLinkAlert from './components/Page/ShareLinkAlert';
 import HotkeysManager from './components/Hotkeys/HotkeysManager';
 import HotkeysManager from './components/Hotkeys/HotkeysManager';
-import Fab from './components/Fab';
 
 
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
 import SocketIoContainer from './services/SocketIoContainer';
 import SocketIoContainer from './services/SocketIoContainer';
@@ -46,7 +45,6 @@ const componentMappings = {
 
 
   'grw-sidebar-wrapper': <Sidebar />,
   'grw-sidebar-wrapper': <Sidebar />,
 
 
-  'grw-fab-container': <Fab />,
   'grw-hotkeys-manager': <HotkeysManager />,
   'grw-hotkeys-manager': <HotkeysManager />,
 
 
   'share-link-alert': <ShareLinkAlert />,
   'share-link-alert': <ShareLinkAlert />,

+ 1 - 1
src/client/js/components/Admin/Customize/Customize.jsx

@@ -10,7 +10,7 @@ import { toastError } from '../../../util/apiNotification';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import toArrayIfNot from '../../../../../lib/util/toArrayIfNot';
 import { withLoadingSppiner } from '../../SuspenseUtils';
 import { withLoadingSppiner } from '../../SuspenseUtils';
 
 
-import CustomizeLayoutSetting from './CustomizeLayoutSetting';
+import CustomizeLayoutSetting from './CustomizeThemeSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeFunctionSetting from './CustomizeFunctionSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeHighlightSetting from './CustomizeHighlightSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';
 import CustomizeCssSetting from './CustomizeCssSetting';

+ 0 - 48
src/client/js/components/Admin/Customize/CustomizeLayoutOption.jsx

@@ -1,48 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-class CustomizeLayoutOption extends React.Component {
-
-  render() {
-    const { layoutType } = this.props;
-
-    return (
-      <React.Fragment>
-        <h4>
-          <div className="custom-control custom-radio">
-            <input
-              type="radio"
-              className="custom-control-input"
-              id={`radio-layout-${layoutType}`}
-              checked={this.props.isSelected}
-              onChange={this.props.onSelected}
-            />
-            <label className="custom-control-label" htmlFor={`radio-layout-${layoutType}`}>
-              {/* eslint-disable-next-line react/no-danger */}
-              <span dangerouslySetInnerHTML={{ __html: this.props.labelHtml }} />
-            </label>
-          </div>
-        </h4>
-        <a href={`/images/admin/customize/layout-${layoutType}.gif`} className="ss-container">
-          <img src={`/images/admin/customize/layout-${layoutType}-thumb.gif`} width="240px" />
-        </a>
-        {/* render layout description */}
-        {this.props.children}
-      </React.Fragment>
-    );
-  }
-
-}
-
-CustomizeLayoutOption.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-
-  layoutType: PropTypes.string.isRequired,
-  labelHtml: PropTypes.string.isRequired,
-  isSelected: PropTypes.bool.isRequired,
-  onSelected: PropTypes.func.isRequired,
-  children: PropTypes.array.isRequired,
-};
-
-export default withTranslation()(CustomizeLayoutOption);

+ 0 - 67
src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx

@@ -1,67 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-import { withTranslation } from 'react-i18next';
-
-import { withUnstatedContainers } from '../../UnstatedUtils';
-import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
-import AppContainer from '../../../services/AppContainer';
-
-import CustomizeLayoutOption from './CustomizeLayoutOption';
-
-class CustomizeLayoutOptions extends React.Component {
-
-  render() {
-    const { t, adminCustomizeContainer } = this.props;
-
-    return (
-      <div className="row row-cols-1 row-cols-md-2">
-        <div className="col text-center">
-          <CustomizeLayoutOption
-            layoutType="crowi-plus"
-            isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
-            onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
-            labelHtml={`GROWI enhanced layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
-          >
-            <h4>{t('admin:customize_setting.layout_desc.growi_title')}</h4>
-            <div className="text-justify d-inline-block">
-              <ul>
-                <li>{t('admin:customize_setting.layout_desc.growi_text1')}</li>
-                <li>{t('admin:customize_setting.layout_desc.growi_text2')}</li>
-                <li>{t('admin:customize_setting.layout_desc.growi_text3')}</li>
-              </ul>
-            </div>
-          </CustomizeLayoutOption>
-        </div>
-
-        <div className="col text-center">
-          <CustomizeLayoutOption
-            layoutType="kibela"
-            isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
-            onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
-            labelHtml="Kibela like layout"
-          >
-            <h4>{t('admin:customize_setting.layout_desc.kibela_title')}</h4>
-            <div className="text-justify d-inline-block">
-              <ul>
-                <li>{t('admin:customize_setting.layout_desc.kibela_text1')}</li>
-                <li>{t('admin:customize_setting.layout_desc.kibela_text2')}</li>
-                <li>{t('admin:customize_setting.layout_desc.kibela_text3')}</li>
-              </ul>
-            </div>
-          </CustomizeLayoutOption>
-        </div>
-      </div>
-    );
-  }
-
-}
-
-const CustomizeLayoutOptionsWrapper = withUnstatedContainers(CustomizeLayoutOptions, [AppContainer, AdminCustomizeContainer]);
-
-CustomizeLayoutOptions.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
-};
-
-export default withTranslation()(CustomizeLayoutOptionsWrapper);

+ 2 - 0
src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -38,6 +38,8 @@ class CustomizeThemeOptions extends React.Component {
       name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
       name: 'future',     bg: '#16282d', topbar: '#2a2929', sidebar: '#00b5b7', theme: '#00b5b7',
     }, {
     }, {
       name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
       name: 'halloween',  bg: '#030003', topbar: '#aa4a04', sidebar: '#162b33', theme: '#e9af2b',
+    }, {
+      name: 'kibela',  bg: '#f4f5f6', topbar: '#1256a3', sidebar: '#5882fa', theme: '#b5cbf79c',
     }];
     }];
     /* eslint-enable no-multi-spaces */
     /* eslint-enable no-multi-spaces */
 
 

+ 6 - 13
src/client/js/components/Admin/Customize/CustomizeLayoutSetting.jsx → src/client/js/components/Admin/Customize/CustomizeThemeSetting.jsx

@@ -7,12 +7,11 @@ import { toastSuccess, toastError } from '../../../util/apiNotification';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 
 
-import CustomizeLayoutOptions from './CustomizeLayoutOptions';
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 import CustomizeThemeOptions from './CustomizeThemeOptions';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminCustomizeContainer from '../../../services/AdminCustomizeContainer';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 import AdminUpdateButtonRow from '../Common/AdminUpdateButtonRow';
 
 
-class CustomizeLayoutSetting extends React.Component {
+class CustomizeThemeSetting extends React.Component {
 
 
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
@@ -24,8 +23,8 @@ class CustomizeLayoutSetting extends React.Component {
     const { t, adminCustomizeContainer } = this.props;
     const { t, adminCustomizeContainer } = this.props;
 
 
     try {
     try {
-      await adminCustomizeContainer.updateCustomizeLayoutAndTheme();
-      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.layout') }));
+      await adminCustomizeContainer.updateCustomizeTheme();
+      toastSuccess(t('toaster.update_successed', { target: t('admin:customize_setting.theme') }));
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
@@ -48,12 +47,6 @@ class CustomizeLayoutSetting extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <div className="row">
-          <div className="col-12">
-            <h2 className="admin-setting-header">{t('admin:customize_setting.layout')}</h2>
-            <CustomizeLayoutOptions />
-          </div>
-        </div>
         <div className="row">
         <div className="row">
           <div className="col-12">
           <div className="col-12">
             <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
             <h2 className="admin-setting-header">{t('admin:customize_setting.theme')}</h2>
@@ -68,12 +61,12 @@ class CustomizeLayoutSetting extends React.Component {
 
 
 }
 }
 
 
-const CustomizeLayoutSettingWrapper = withUnstatedContainers(CustomizeLayoutSetting, [AppContainer, AdminCustomizeContainer]);
+const CustomizeThemeSettingWrapper = withUnstatedContainers(CustomizeThemeSetting, [AppContainer, AdminCustomizeContainer]);
 
 
-CustomizeLayoutSetting.propTypes = {
+CustomizeThemeSetting.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
   adminCustomizeContainer: PropTypes.instanceOf(AdminCustomizeContainer).isRequired,
 };
 };
 
 
-export default withTranslation()(CustomizeLayoutSettingWrapper);
+export default withTranslation()(CustomizeThemeSettingWrapper);

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

@@ -36,14 +36,15 @@ class ManageExternalAccount extends React.Component {
     const { t, adminExternalAccountsContainer } = this.props;
     const { t, adminExternalAccountsContainer } = this.props;
 
 
     const pager = (
     const pager = (
-      <div className="pull-right">
-        <PaginationWrapper
-          activePage={adminExternalAccountsContainer.state.activePage}
-          changePage={this.handleExternalAccountPage}
-          totalItemsCount={adminExternalAccountsContainer.state.totalAccounts}
-          pagingLimit={adminExternalAccountsContainer.state.pagingLimit}
-        />
-      </div>
+
+      <PaginationWrapper
+        activePage={adminExternalAccountsContainer.state.activePage}
+        changePage={this.handleExternalAccountPage}
+        totalItemsCount={adminExternalAccountsContainer.state.totalAccounts}
+        pagingLimit={adminExternalAccountsContainer.state.pagingLimit}
+        align="right"
+      />
+
     );
     );
     return (
     return (
       <Fragment>
       <Fragment>

+ 33 - 28
src/client/js/components/Admin/Security/ShareLinkSetting.jsx

@@ -66,6 +66,7 @@ class ShareLinkSetting extends React.Component {
 
 
   async deleteLinkById(shareLinkId) {
   async deleteLinkById(shareLinkId) {
     const { t, appContainer, adminGeneralSecurityContainer } = this.props;
     const { t, appContainer, adminGeneralSecurityContainer } = this.props;
+    const { shareLinksActivePage } = adminGeneralSecurityContainer.state;
 
 
     try {
     try {
       const res = await appContainer.apiv3Delete(`/share-links/${shareLinkId}`);
       const res = await appContainer.apiv3Delete(`/share-links/${shareLinkId}`);
@@ -76,53 +77,57 @@ class ShareLinkSetting extends React.Component {
       toastError(err);
       toastError(err);
     }
     }
 
 
-    this.getShareLinkList(adminGeneralSecurityContainer.state.shareLinksActivePage);
+    this.getShareLinkList(shareLinksActivePage);
   }
   }
 
 
 
 
   render() {
   render() {
     const { t, adminGeneralSecurityContainer } = this.props;
     const { t, adminGeneralSecurityContainer } = this.props;
-
-    const pager = (
-      <div className="pull-right my-3">
+    const {
+      shareLinks, shareLinksActivePage, totalshareLinks, shareLinksPagingLimit,
+    } = adminGeneralSecurityContainer.state;
+
+    function pager() {
+      if (shareLinks.length === 0) {
+        return null;
+      }
+      return (
         <PaginationWrapper
         <PaginationWrapper
-          activePage={adminGeneralSecurityContainer.state.shareLinksActivePage}
+          activePage={shareLinksActivePage}
           changePage={this.getShareLinkList}
           changePage={this.getShareLinkList}
-          totalItemsCount={adminGeneralSecurityContainer.state.totalshareLinks}
-          pagingLimit={adminGeneralSecurityContainer.state.shareLinksPagingLimit}
+          totalItemsCount={totalshareLinks}
+          pagingLimit={shareLinksPagingLimit}
+          align="right"
         />
         />
-      </div>
-    );
+      );
+    }
 
 
-    const deleteAllButton = (
-      adminGeneralSecurityContainer.state.shareLinks.length > 0
-        ? (
+    return (
+      <Fragment>
+        <div className="mb-3">
           <button
           <button
             className="pull-right btn btn-danger"
             className="pull-right btn btn-danger"
+            disabled={shareLinks.length === 0}
             type="button"
             type="button"
             onClick={this.showDeleteConfirmModal}
             onClick={this.showDeleteConfirmModal}
           >
           >
             {t('share_links.delete_all_share_links')}
             {t('share_links.delete_all_share_links')}
           </button>
           </button>
-        )
-        : (
-          <p className="pull-right mr-2">{t('share_links.No_share_links')}</p>
-        )
-    );
-
-    return (
-      <Fragment>
-        <div className="mb-3">
-          {deleteAllButton}
           <h2 className="alert-anchor border-bottom">{t('share_links.share_link_management')}</h2>
           <h2 className="alert-anchor border-bottom">{t('share_links.share_link_management')}</h2>
         </div>
         </div>
-
         {pager}
         {pager}
-        <ShareLinkList
-          shareLinks={adminGeneralSecurityContainer.state.shareLinks}
-          onClickDeleteButton={this.deleteLinkById}
-          isAdmin
-        />
+
+        {(shareLinks.length !== 0) ? (
+          <ShareLinkList
+            shareLinks={shareLinks}
+            onClickDeleteButton={this.deleteLinkById}
+            isAdmin
+          />
+          )
+          : (<p className="text-center">{t('share_links.No_share_links')}</p>
+          )
+        }
+
 
 
         <DeleteAllShareLinksModal
         <DeleteAllShareLinksModal
           isOpen={this.state.isDeleteConfirmModalShown}
           isOpen={this.state.isDeleteConfirmModalShown}

+ 2 - 1
src/client/js/components/Admin/UserManagement.jsx

@@ -114,12 +114,13 @@ class UserManagement extends React.Component {
     const { t, adminUsersContainer } = this.props;
     const { t, adminUsersContainer } = this.props;
 
 
     const pager = (
     const pager = (
-      <div className="pull-right my-3">
+      <div className="my-3">
         <PaginationWrapper
         <PaginationWrapper
           activePage={adminUsersContainer.state.activePage}
           activePage={adminUsersContainer.state.activePage}
           changePage={this.handlePage}
           changePage={this.handlePage}
           totalItemsCount={adminUsersContainer.state.totalUsers}
           totalItemsCount={adminUsersContainer.state.totalUsers}
           pagingLimit={adminUsersContainer.state.pagingLimit}
           pagingLimit={adminUsersContainer.state.pagingLimit}
+          align="right"
         />
         />
       </div>
       </div>
     );
     );

+ 21 - 11
src/client/js/components/Fab.jsx

@@ -4,6 +4,7 @@ import loggerFactory from '@alias/logger';
 
 
 import StickyEvents from 'sticky-events';
 import StickyEvents from 'sticky-events';
 
 
+import AppContainer from '../services/AppContainer';
 import NavigationContainer from '../services/NavigationContainer';
 import NavigationContainer from '../services/NavigationContainer';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import CreatePageIcon from './Icons/CreatePageIcon';
 import CreatePageIcon from './Icons/CreatePageIcon';
@@ -12,7 +13,8 @@ import ReturnTopIcon from './Icons/ReturnTopIcon';
 const logger = loggerFactory('growi:cli:Fab');
 const logger = loggerFactory('growi:cli:Fab');
 
 
 const Fab = (props) => {
 const Fab = (props) => {
-  const { navigationContainer } = props;
+  const { navigationContainer, appContainer } = props;
+  const { currentUser } = appContainer;
 
 
   const [animateClasses, setAnimateClasses] = useState('invisible');
   const [animateClasses, setAnimateClasses] = useState('invisible');
 
 
@@ -39,18 +41,25 @@ const Fab = (props) => {
     };
     };
   }, [stickyChangeHandler]);
   }, [stickyChangeHandler]);
 
 
+  function renderPageCreateButton() {
+    return (
+      <>
+        <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
+          <button
+            type="button"
+            className="btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light"
+            onClick={navigationContainer.openPageCreateModal}
+          >
+            <CreatePageIcon />
+          </button>
+        </div>
+      </>
+    );
+  }
 
 
   return (
   return (
     <div className="grw-fab d-none d-md-block">
     <div className="grw-fab d-none d-md-block">
-      <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: '2.3rem', right: '4rem' }}>
-        <button
-          type="button"
-          className="btn btn-lg btn-create-page btn-primary rounded-circle p-0 waves-effect waves-light"
-          onClick={navigationContainer.openPageCreateModal}
-        >
-          <CreatePageIcon />
-        </button>
-      </div>
+      {currentUser != null && renderPageCreateButton()}
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
       <div className={`rounded-circle position-absolute ${animateClasses}`} style={{ bottom: 0, right: 0 }}>
         <button type="button" className="btn btn-light btn-scroll-to-top rounded-circle p-0" onClick={() => navigationContainer.smoothScrollIntoView()}>
         <button type="button" className="btn btn-light btn-scroll-to-top rounded-circle p-0" onClick={() => navigationContainer.smoothScrollIntoView()}>
           <ReturnTopIcon />
           <ReturnTopIcon />
@@ -62,7 +71,8 @@ const Fab = (props) => {
 };
 };
 
 
 Fab.propTypes = {
 Fab.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
   navigationContainer: PropTypes.instanceOf(NavigationContainer).isRequired,
 };
 };
 
 
-export default withUnstatedContainers(Fab, [NavigationContainer]);
+export default withUnstatedContainers(Fab, [AppContainer, NavigationContainer]);

+ 16 - 0
src/client/js/components/Icons/PagePreviewIcon.jsx

@@ -0,0 +1,16 @@
+import React from 'react';
+
+const PagePreviewIcon = () => (
+  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23">
+    <defs></defs>
+    <rect width="23" height="23" fillOpacity="0" />
+    <path d="M10.94,20.33H3.4V1.38H8.82V8.82h7.44v1.35a6.16,6.16,0,0,1,1.35.47V6.79L10.85,0H3.4a1.3,1.3,0,0,0-1,.39,1.3,1.3,0,0,0-.39,1v19A1.33,
+  1.33,0,0,0,3.4,21.68h9.84A5.94,5.94,0,0,1,10.94,20.33ZM10.17,1.38h.13l6,6v.11H10.17Z"
+    />
+    <path d="M21.87,22.14,18.75,19a4.74,4.74,0,0,0,1.1-3,4.89,4.89,0,1,0-1.8,3.73l3.11,3.11a.5.5,0,0,0,.35.15.51.51,0,0,0,.36-.15A.5.5,
+  0,0,0,21.87,22.14ZM15,19.57A3.57,3.57,0,1,1,18.59,16,3.58,3.58,0,0,1,15,19.57Z"
+    />
+  </svg>
+);
+
+export default PagePreviewIcon;

+ 22 - 0
src/client/js/components/Icons/PresentationIcon.jsx

@@ -0,0 +1,22 @@
+import React from 'react';
+
+const PresentationIcon = () => (
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    width="12.25"
+    height="14"
+    viewBox="0 0 12.25 14"
+  >
+    <path
+      d="M44.261,0H32.909a.448.448,0,0,0-.449.448V7.635a.449.449,0,0,0,.9,0V.9H43.812V7.635a.449.449,0,0,0,.9,0V.448A.448.448,0,0,0,44.261,0Z"
+      transform="translate(-32.46)"
+    />
+    <path
+      d="M90.959,287.182H82.315a.448.448,0,1,0,0,.9h3.873v1.115l-3.207,3.381a.449.449,0,0,0,.652.616l2.555-2.694v2.013a.449.449,0,0,0,.9,0V
+        290.5l2.555,2.694a.449.449,0,0,0,.652-.616l-3.208-3.382v-1.114h3.873a.448.448,0,1,0,0-.9Z"
+      transform="translate(-80.512 -279.329)"
+    />
+  </svg>
+);
+
+export default PresentationIcon;

+ 4 - 8
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -18,6 +18,7 @@ import RevisionPathControls from '../Page/RevisionPathControls';
 import TagLabels from '../Page/TagLabels';
 import TagLabels from '../Page/TagLabels';
 import LikeButton from '../LikeButton';
 import LikeButton from '../LikeButton';
 import BookmarkButton from '../BookmarkButton';
 import BookmarkButton from '../BookmarkButton';
+import ThreeStrandedButton from './ThreeStrandedButton';
 
 
 import PageCreator from './PageCreator';
 import PageCreator from './PageCreator';
 import RevisionAuthor from './RevisionAuthor';
 import RevisionAuthor from './RevisionAuthor';
@@ -186,14 +187,9 @@ const GrowiSubNavigation = (props) => {
         <div className="d-flex flex-column align-items-end justify-content-center">
         <div className="d-flex flex-column align-items-end justify-content-center">
           <div className="d-flex">
           <div className="d-flex">
             { !isPageInTrash && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
             { !isPageInTrash && <PageReactionButtons appContainer={appContainer} pageContainer={pageContainer} /> }
-            <div className="mt-2">
-              {/* TODO: impl View / Edit / HackMD button group */}
-              {/* <div className="btn-group" role="group" aria-label="Basic example">
-              <button type="button" className="btn btn-outline-primary">Left</button>
-              <button type="button" className="btn btn-outline-primary">Middle</button>
-              <button type="button" className="btn btn-outline-primary">Right</button>
-            </div> */}
-            </div>
+          </div>
+          <div className="mt-2">
+            <ThreeStrandedButton />
           </div>
           </div>
         </div>
         </div>
 
 

+ 39 - 0
src/client/js/components/Navbar/ThreeStrandedButton.jsx

@@ -0,0 +1,39 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+const ThreeStrandedButton = (props) => {
+
+  const { t } = props;
+
+  function threeStrandedButtonClickedHandler(viewType) {
+    if (props.onThreeStrandedButtonClicked != null) {
+      props.onThreeStrandedButtonClicked(viewType);
+    }
+  }
+
+  return (
+    <div className="btn-group grw-three-stranded-button" role="group " aria-label="three-stranded-button">
+      <button type="button" className="btn btn-outline-primary view-button" onClick={() => { threeStrandedButtonClickedHandler('view') }}>
+        <i className="icon-control-play icon-fw" />
+        { t('view') }
+      </button>
+      <button type="button" className="btn btn-outline-primary edit-button" onClick={() => { threeStrandedButtonClickedHandler('edit') }}>
+        <i className="icon-note icon-fw" />
+        { t('Edit') }
+      </button>
+      <button type="button" className="btn btn-outline-primary hackmd-button" onClick={() => { threeStrandedButtonClickedHandler('hackmd') }}>
+        <i className="fa fa-fw fa-file-text-o" />
+        { t('hackmd.hack_md') }
+      </button>
+    </div>
+  );
+
+};
+
+ThreeStrandedButton.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  onThreeStrandedButtonClicked: PropTypes.func,
+};
+
+export default withTranslation()(ThreeStrandedButton);

+ 18 - 2
src/client/js/components/Page/PageManagement.jsx

@@ -12,6 +12,8 @@ import PageDeleteModal from '../PageDeleteModal';
 import PageRenameModal from '../PageRenameModal';
 import PageRenameModal from '../PageRenameModal';
 import PageDuplicateModal from '../PageDuplicateModal';
 import PageDuplicateModal from '../PageDuplicateModal';
 import CreateTemplateModal from '../CreateTemplateModal';
 import CreateTemplateModal from '../CreateTemplateModal';
+import PagePresentationModal from '../PagePresentationModal';
+import PresentationIcon from '../Icons/PresentationIcon';
 
 
 
 
 const PageManagement = (props) => {
 const PageManagement = (props) => {
@@ -25,6 +27,7 @@ const PageManagement = (props) => {
   const [isPageDuplicateModalShown, setIsPageDuplicateModalShown] = useState(false);
   const [isPageDuplicateModalShown, setIsPageDuplicateModalShown] = useState(false);
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
   const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
   const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
   const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
+  const [isPagePresentationModalShown, setIsPagePresentationModalShown] = useState(false);
 
 
   function openPageRenameModalHandler() {
   function openPageRenameModalHandler() {
     setIsPageRenameModalShown(true);
     setIsPageRenameModalShown(true);
@@ -58,6 +61,14 @@ const PageManagement = (props) => {
     setIsPageDeleteModalShown(false);
     setIsPageDeleteModalShown(false);
   }
   }
 
 
+  function openPagePresentationModalHandler() {
+    setIsPagePresentationModalShown(true);
+  }
+
+  function closePagePresentationModalHandler() {
+    setIsPagePresentationModalShown(false);
+  }
+
 
 
   // TODO GW-2746 bulk export pages
   // TODO GW-2746 bulk export pages
   // async function getArchivePageData() {
   // async function getArchivePageData() {
@@ -98,8 +109,8 @@ const PageManagement = (props) => {
         <button className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
         <button className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
           <i className="icon-fw icon-docs"></i> { t('Duplicate') }
           <i className="icon-fw icon-docs"></i> { t('Duplicate') }
         </button>
         </button>
-        <button className="dropdown-item toggle-presentation" type="button" href="?presentation=1">
-          <i className="icon-film icon-fw"></i><span className="d-none d-sm-inline">{ t('Presentation Mode') }</span>
+        <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>
         <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')}
@@ -150,6 +161,11 @@ const PageManagement = (props) => {
           path={path}
           path={path}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
         />
         />
+        <PagePresentationModal
+          isOpen={isPagePresentationModalShown}
+          onClose={closePagePresentationModalHandler}
+          href="?presentation=1"
+        />
       </>
       </>
     );
     );
   }
   }

+ 91 - 62
src/client/js/components/PageAccessoriesModal.jsx

@@ -1,8 +1,8 @@
-import React from 'react';
+import React, { useEffect, useState } from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
 import {
 import {
-  Modal, ModalBody, Nav, NavItem, NavLink, TabContent, TabPane,
+  Modal, ModalBody, ModalHeader, Nav, NavItem, NavLink, TabContent, TabPane,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
@@ -21,11 +21,43 @@ 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,
+  },
+};
+
 const PageAccessoriesModal = (props) => {
 const PageAccessoriesModal = (props) => {
   const { t, pageAccessoriesContainer } = props;
   const { t, pageAccessoriesContainer } = props;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { switchActiveTab } = pageAccessoriesContainer;
   const { activeTab } = pageAccessoriesContainer.state;
   const { activeTab } = pageAccessoriesContainer.state;
 
 
+  const [sliderWidth, setSliderWidth] = useState(null);
+  const [sliderMarginLeft, setSliderMarginLeft] = useState(null);
+
   function closeModalHandler() {
   function closeModalHandler() {
     if (props.onClose == null) {
     if (props.onClose == null) {
       return;
       return;
@@ -33,80 +65,77 @@ const PageAccessoriesModal = (props) => {
     props.onClose();
     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]);
+
+
   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">
-        <ModalBody>
-          <Nav className="nav-title border-bottom">
-            <NavItem type="button" className={`nav-link ${activeTab === 'pagelist' && 'active active-border'}`}>
-              <NavLink
-                onClick={() => {
-                  switchActiveTab('pagelist');
-                }}
-              >
-                <PageListIcon />
-                {t('page_list')}
-              </NavLink>
-            </NavItem>
-            <NavItem type="button" className={`nav-link ${activeTab === 'timeline' && 'active active-border'}`}>
-              <NavLink
-                onClick={() => {
-                  switchActiveTab('timeline');
-                }}
-              >
-                <TimeLineIcon />
-                {t('Timeline View')}
-              </NavLink>
-            </NavItem>
-            <NavItem type="button" className={`nav-link ${activeTab === 'page-history' && 'active active-border'}`}>
-              <NavLink
-                onClick={() => {
-                  switchActiveTab('page-history');
-                }}
-              >
-                <RecentChangesIcon />
-                {t('History')}
-              </NavLink>
-            </NavItem>
-            <NavItem type="button" className={`nav-link ${activeTab === 'attachment' && 'active active-border'}`}>
-              <NavLink
-                onClick={() => {
-                  switchActiveTab('attachment');
-                }}
-              >
-                <AttachmentIcon />
-                {t('attachment_data')}
-              </NavLink>
-            </NavItem>
-            <NavItem type="button" className={`nav-link ${activeTab === 'share-link' && 'active active-border'}`}>
-              <NavLink
-                onClick={() => {
-                  switchActiveTab('share-link');
-                }}
-              >
-                <ShareLinkIcon />
-                {t('share_links.share_link_management')}
-              </NavLink>
-            </NavItem>
+        {/* [TODO: insert a modal header and move nav tabs there  by gw-3890] */}
+        <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>
           </Nav>
-          <TabContent activeTab={activeTab}>
-
+          <hr className="my-0 grw-nav-slide-hr border-none" style={{ width: `${sliderWidth}%`, marginLeft: `${sliderMarginLeft}%` }} />
+        </ModalHeader>
+        <ModalBody className="overflow-auto grw-modal-body-style p-0">
+          <TabContent activeTab={activeTab} className="p-5">
             <TabPane tabId="pagelist">
             <TabPane tabId="pagelist">
               {pageAccessoriesContainer.state.activeComponents.has('pagelist') && <PageList />}
               {pageAccessoriesContainer.state.activeComponents.has('pagelist') && <PageList />}
             </TabPane>
             </TabPane>
-            <TabPane tabId="timeline" className="p-4">
+            <TabPane tabId="timeline">
               {pageAccessoriesContainer.state.activeComponents.has('timeline') && <PageTimeline /> }
               {pageAccessoriesContainer.state.activeComponents.has('timeline') && <PageTimeline /> }
             </TabPane>
             </TabPane>
-            <TabPane tabId="page-history">
+            <TabPane tabId="pageHistory">
               <div className="overflow-auto">
               <div className="overflow-auto">
-                {pageAccessoriesContainer.state.activeComponents.has('page-history') && <PageHistory /> }
+                {pageAccessoriesContainer.state.activeComponents.has('pageHistory') && <PageHistory /> }
               </div>
               </div>
             </TabPane>
             </TabPane>
-            <TabPane tabId="attachment" className="p-4">
+            <TabPane tabId="attachment">
               {pageAccessoriesContainer.state.activeComponents.has('attachment') && <PageAttachment />}
               {pageAccessoriesContainer.state.activeComponents.has('attachment') && <PageAttachment />}
             </TabPane>
             </TabPane>
-            <TabPane tabId="share-link" className="p-4">
-              {pageAccessoriesContainer.state.activeComponents.has('share-link') && <ShareLink />}
+            <TabPane tabId="shareLink">
+              {pageAccessoriesContainer.state.activeComponents.has('shareLink') && <ShareLink />}
             </TabPane>
             </TabPane>
           </TabContent>
           </TabContent>
         </ModalBody>
         </ModalBody>

+ 16 - 4
src/client/js/components/PageAttachment.jsx

@@ -1,6 +1,7 @@
 /* eslint-disable react/no-access-state-in-setstate */
 /* eslint-disable react/no-access-state-in-setstate */
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
 
 
 import PageAttachmentList from './PageAttachment/PageAttachmentList';
 import PageAttachmentList from './PageAttachment/PageAttachmentList';
 import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
 import DeleteAttachmentModal from './PageAttachment/DeleteAttachmentModal';
@@ -62,6 +63,9 @@ class PageAttachment extends React.Component {
 
 
   async componentDidMount() {
   async componentDidMount() {
     await this.handlePage(1);
     await this.handlePage(1);
+    this.setState({
+      activePage: 1,
+    });
   }
   }
 
 
   checkIfFileInUse(attachment) {
   checkIfFileInUse(attachment) {
@@ -110,6 +114,13 @@ class PageAttachment extends React.Component {
 
 
 
 
   render() {
   render() {
+
+    const { t } = this.props;
+    if (this.state.attachments.length === 0) {
+      return t('No_attachments_yet');
+
+    }
+
     let deleteAttachmentModal = '';
     let deleteAttachmentModal = '';
     if (this.isUserLoggedIn()) {
     if (this.isUserLoggedIn()) {
       const attachmentToDelete = this.state.attachmentToDelete;
       const attachmentToDelete = this.state.attachmentToDelete;
@@ -138,9 +149,8 @@ class PageAttachment extends React.Component {
       );
       );
     }
     }
 
 
-
     return (
     return (
-      <div>
+      <>
         <PageAttachmentList
         <PageAttachmentList
           attachments={this.state.attachments}
           attachments={this.state.attachments}
           inUse={this.state.inUse}
           inUse={this.state.inUse}
@@ -155,8 +165,9 @@ class PageAttachment extends React.Component {
           changePage={this.handlePage}
           changePage={this.handlePage}
           totalItemsCount={this.state.totalAttachments}
           totalItemsCount={this.state.totalAttachments}
           pagingLimit={this.state.limit}
           pagingLimit={this.state.limit}
+          align="center"
         />
         />
-      </div>
+      </>
     );
     );
   }
   }
 
 
@@ -169,8 +180,9 @@ const PageAttachmentWrapper = withUnstatedContainers(PageAttachment, [AppContain
 
 
 
 
 PageAttachment.propTypes = {
 PageAttachment.propTypes = {
+  t: PropTypes.func.isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
   pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 };
 
 
-export default PageAttachmentWrapper;
+export default withTranslation()(PageAttachmentWrapper);

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

@@ -876,7 +876,7 @@ export default class CodeMirrorEditor extends AbstractEditor {
 
 
         <LinkEditModal
         <LinkEditModal
           ref={this.linkEditModal}
           ref={this.linkEditModal}
-          onSave={(link) => { return mlu.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), link) }}
+          onSave={(linkText) => { return mlu.replaceFocusedMarkdownLinkWithEditor(this.getCodeMirror(), linkText) }}
         />
         />
         <HandsontableModal
         <HandsontableModal
           ref={this.handsontableModal}
           ref={this.handsontableModal}

+ 270 - 150
src/client/js/components/PageEditor/LinkEditModal.jsx

@@ -5,13 +5,16 @@ import {
   Modal,
   Modal,
   ModalHeader,
   ModalHeader,
   ModalBody,
   ModalBody,
-  ModalFooter,
+  Popover,
+  PopoverBody,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
 import { debounce } from 'throttle-debounce';
 import { debounce } from 'throttle-debounce';
 
 
 import path from 'path';
 import path from 'path';
+import validator from 'validator';
 import Preview from './Preview';
 import Preview from './Preview';
+import PagePreviewIcon from '../Icons/PagePreviewIcon';
 
 
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import PageContainer from '../../services/PageContainer';
@@ -34,7 +37,10 @@ class LinkEditModal extends React.PureComponent {
       labelInputValue: '',
       labelInputValue: '',
       linkerType: Linker.types.markdownLink,
       linkerType: Linker.types.markdownLink,
       markdown: '',
       markdown: '',
+      previewError: '',
       permalink: '',
       permalink: '',
+      linkText: '',
+      isPreviewOpen: false,
     };
     };
 
 
     this.isApplyPukiwikiLikeLinkerPlugin = window.growiRenderer.preProcessors.some(process => process.constructor.name === 'PukiwikiLikeLinker');
     this.isApplyPukiwikiLikeLinkerPlugin = window.growiRenderer.preProcessors.some(process => process.constructor.name === 'PukiwikiLikeLinker');
@@ -52,47 +58,78 @@ class LinkEditModal extends React.PureComponent {
     this.generateLink = this.generateLink.bind(this);
     this.generateLink = this.generateLink.bind(this);
     this.renderPreview = this.renderPreview.bind(this);
     this.renderPreview = this.renderPreview.bind(this);
     this.getRootPath = this.getRootPath.bind(this);
     this.getRootPath = this.getRootPath.bind(this);
-
-    this.getPreviewDebounced = debounce(200, this.getPreview.bind(this));
+    this.toggleIsPreviewOpen = this.toggleIsPreviewOpen.bind(this);
+    this.generateAndSetPreviewDebounced = debounce(200, this.generateAndSetPreview.bind(this));
   }
   }
 
 
   componentDidUpdate(prevProps, prevState) {
   componentDidUpdate(prevProps, prevState) {
     const { linkInputValue: prevLinkInputValue } = prevState;
     const { linkInputValue: prevLinkInputValue } = prevState;
     const { linkInputValue } = this.state;
     const { linkInputValue } = this.state;
     if (linkInputValue !== prevLinkInputValue) {
     if (linkInputValue !== prevLinkInputValue) {
-      this.getPreviewDebounced(linkInputValue);
+      this.generateAndSetPreviewDebounced(linkInputValue);
     }
     }
   }
   }
 
 
   // defaultMarkdownLink is an instance of Linker
   // defaultMarkdownLink is an instance of Linker
   show(defaultMarkdownLink = null) {
   show(defaultMarkdownLink = null) {
     // if defaultMarkdownLink is null, set default value in inputs.
     // if defaultMarkdownLink is null, set default value in inputs.
-    const { label = '' } = defaultMarkdownLink;
-    let { link = '', type = Linker.types.markdownLink } = defaultMarkdownLink;
+    const { label = '', link = '' } = defaultMarkdownLink;
+    let { type = Linker.types.markdownLink } = defaultMarkdownLink;
 
 
     // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link)
     // if type of defaultMarkdownLink is pukiwikiLink when pukiwikiLikeLinker plugin is disable, change type(not change label and link)
     if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) {
     if (type === Linker.types.pukiwikiLink && !this.isApplyPukiwikiLikeLinkerPlugin) {
       type = Linker.types.markdownLink;
       type = Linker.types.markdownLink;
     }
     }
 
 
-    const url = new URL(link, 'http://example.com');
-    const isUseRelativePath = url.origin === 'http://example.com' && !link.startsWith('/') && link !== '';
-    if (isUseRelativePath) {
-      const rootPath = this.getRootPath(type);
-      link = path.resolve(rootPath, link);
-    }
+    this.parseLinkAndSetState(link, type);
 
 
     this.setState({
     this.setState({
       show: true,
       show: true,
       labelInputValue: label,
       labelInputValue: label,
-      linkInputValue: link,
       isUsePermanentLink: false,
       isUsePermanentLink: false,
       permalink: '',
       permalink: '',
       linkerType: type,
       linkerType: type,
+    });
+  }
+
+  // parse link, link is ...
+  // case-1. url of this growi's page (ex. 'http://localhost:3000/hoge/fuga')
+  // case-2. absolute path of this growi's page (ex. '/hoge/fuga')
+  // case-3. relative path of this growi's page (ex. '../fuga', 'hoge')
+  // case-4. external link (ex. 'https://growi.org')
+  // case-5. the others (ex. '')
+  parseLinkAndSetState(link, type) {
+    // create url from link, add dummy origin if link is not valid url.
+    // ex-1. link = 'https://growi.org/' -> url = 'https://growi.org/' (case-1,4)
+    // ex-2. link = 'hoge' -> url = 'http://example.com/hoge' (case-2,3,5)
+    const url = new URL(link, 'http://example.com');
+    const isUrl = url.origin !== 'http://example.com';
+
+    let isUseRelativePath = false;
+    let reshapedLink = link;
+
+    // if case-1, reshapedLink becomes page path
+    reshapedLink = this.convertUrlToPathIfPageUrl(reshapedLink, url);
+
+    // case-3
+    if (!isUrl && !reshapedLink.startsWith('/') && reshapedLink !== '') {
+      isUseRelativePath = true;
+      const rootPath = this.getRootPath(type);
+      reshapedLink = path.resolve(rootPath, reshapedLink);
+    }
+
+    this.setState({
+      linkInputValue: reshapedLink,
       isUseRelativePath,
       isUseRelativePath,
     });
     });
   }
   }
 
 
+  // return path name of link if link is this growi page url, else return original link.
+  convertUrlToPathIfPageUrl(link, url) {
+    // when link is this growi's page url, url.origin === window.location.origin and return path name
+    return url.origin === window.location.origin ? decodeURI(url.pathname) : link;
+  }
+
   cancel() {
   cancel() {
     this.hide();
     this.hide();
   }
   }
@@ -122,27 +159,73 @@ class LinkEditModal extends React.PureComponent {
   }
   }
 
 
   renderPreview() {
   renderPreview() {
-    return (
-      <div className="linkedit-preview">
-        <Preview
-          markdown={this.state.markdown}
-        />
-      </div>
-    );
+    if (this.state.markdown !== '') {
+      return (
+        <div className="linkedit-preview">
+          <Preview markdown={this.state.markdown} />
+        </div>
+      );
+    }
+    if (this.state.previewError !== '') {
+      return this.state.previewError;
+    }
+    return 'Page preview here.';
   }
   }
 
 
-  async getPreview(path) {
+  async generateAndSetPreview(path) {
     let markdown = '';
     let markdown = '';
+    let previewError = '';
     let permalink = '';
     let permalink = '';
-    try {
-      const res = await this.props.appContainer.apiGet('/pages.get', { path });
-      markdown = res.page.revision.body;
-      permalink = `${window.location.origin}/${res.page.id}`;
+
+    if (path.startsWith('/')) {
+      const pathWithoutFragment = new URL(path, 'http://dummy').pathname;
+      const isPermanentLink = validator.isMongoId(pathWithoutFragment.slice(1));
+      const pageId = isPermanentLink ? pathWithoutFragment.slice(1) : null;
+
+      try {
+        const { page } = await this.props.appContainer.apiGet('/pages.get', { path: pathWithoutFragment, page_id: pageId });
+        markdown = page.revision.body;
+        // create permanent link only if path isn't permanent link because checkbox for isUsePermanentLink is disabled when permalink is ''.
+        permalink = !isPermanentLink ? `${window.location.origin}/${page.id}` : '';
+      }
+      catch (err) {
+        previewError = err.message;
+      }
+    }
+    this.setState({ markdown, previewError, permalink });
+  }
+
+  renderLinkPreview() {
+    const linker = this.generateLink();
+
+    if (this.isUsePermanentLink && this.permalink != null) {
+      linker.link = this.permalink;
     }
     }
-    catch (err) {
-      markdown = `<div class="alert alert-warning" role="alert"><strong>${err.message}</strong></div>`;
+
+    if (linker.label === '') {
+      linker.label = linker.link;
     }
     }
-    this.setState({ markdown, permalink });
+
+    const linkText = linker.generateMarkdownText();
+    return (
+      <div className="d-flex justify-content-between mb-3">
+        <div className="card card-disabled w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">Markdown</p>
+          <p className="text-center text-truncate text-muted">{linkText}</p>
+        </div>
+        <div className="d-flex align-items-center">
+          <span className="lead mx-3">
+            <i className="fa fa-caret-right"></i>
+          </span>
+        </div>
+        <div className="card w-100 p-1 mb-0">
+          <p className="text-left text-muted mb-1 small">HTML</p>
+          <p className="text-center text-truncate">
+            <a href={linker.link}>{linker.label}</a>
+          </p>
+        </div>
+      </div>
+    );
   }
   }
 
 
   handleChangeTypeahead(selected) {
   handleChangeTypeahead(selected) {
@@ -174,10 +257,8 @@ class LinkEditModal extends React.PureComponent {
   }
   }
 
 
   save() {
   save() {
-    const output = this.generateLink();
-
     if (this.props.onSave != null) {
     if (this.props.onSave != null) {
-      this.props.onSave(output);
+      this.props.onSave(this.state.linkText);
     }
     }
 
 
     this.hide();
     this.hide();
@@ -185,12 +266,7 @@ class LinkEditModal extends React.PureComponent {
 
 
   generateLink() {
   generateLink() {
     const {
     const {
-      linkInputValue,
-      labelInputValue,
-      linkerType,
-      isUseRelativePath,
-      isUsePermanentLink,
-      permalink,
+      linkInputValue, labelInputValue, linkerType, isUseRelativePath, isUsePermanentLink, permalink,
     } = this.state;
     } = this.state;
 
 
     let reshapedLink = linkInputValue;
     let reshapedLink = linkInputValue;
@@ -199,13 +275,11 @@ class LinkEditModal extends React.PureComponent {
       reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue);
       reshapedLink = rootPath === linkInputValue ? '.' : path.relative(rootPath, linkInputValue);
     }
     }
 
 
-    return new Linker(
-      linkerType,
-      labelInputValue,
-      reshapedLink,
-      isUsePermanentLink,
-      permalink,
-    );
+    if (isUsePermanentLink && permalink != null) {
+      reshapedLink = permalink;
+    }
+
+    return new Linker(linkerType, labelInputValue, reshapedLink);
   }
   }
 
 
   getRootPath(type) {
   getRootPath(type) {
@@ -215,124 +289,170 @@ class LinkEditModal extends React.PureComponent {
     return type === Linker.types.markdownLink ? path.dirname(pagePath) : pagePath;
     return type === Linker.types.markdownLink ? path.dirname(pagePath) : pagePath;
   }
   }
 
 
+  toggleIsPreviewOpen() {
+    this.setState({ isPreviewOpen: !this.state.isPreviewOpen });
+  }
+
+  renderLinkAndLabelForm() {
+    return (
+      <>
+        <h3 className="grw-modal-head">Set link and label</h3>
+        <form className="form-group">
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">link</span>
+              </div>
+              <SearchTypeahead
+                onChange={this.handleChangeTypeahead}
+                onInputChange={this.handleChangeLinkInput}
+                inputName="link"
+                placeholder="Input page path or URL"
+                keywordOnInit={this.state.linkInputValue}
+              />
+              <div className="input-group-append">
+                <button type="button" id="preview-btn" className="btn btn-info btn-page-preview">
+                  <PagePreviewIcon />
+                </button>
+                <Popover trigger="focus" placement="right" isOpen={this.state.isPreviewOpen} target="preview-btn" toggle={this.toggleIsPreviewOpen}>
+                  <PopoverBody>
+                    {this.renderPreview()}
+                  </PopoverBody>
+                </Popover>
+              </div>
+            </div>
+          </div>
+          <div className="form-gorup my-3">
+            <div className="input-group flex-nowrap">
+              <div className="input-group-prepend">
+                <span className="input-group-text">label</span>
+              </div>
+              <input
+                type="text"
+                className="form-control"
+                id="label"
+                value={this.state.labelInputValue}
+                onChange={e => this.handleChangeLabelInput(e.target.value)}
+                disabled={this.state.linkerType === Linker.types.growiLink}
+              />
+            </div>
+          </div>
+        </form>
+      </>
+    );
+  }
+
+  renderPathFormatForm() {
+    return (
+      <div className="card well pt-3">
+        <form className="form-group mb-0">
+          <div className="form-group row">
+            <label className="col-sm-3">Path format</label>
+            <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
+              <input
+                className="custom-control-input"
+                id="relativePath"
+                type="checkbox"
+                checked={this.state.isUseRelativePath}
+                onChange={this.toggleIsUseRelativePath}
+                disabled={!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink}
+              />
+              <label className="custom-control-label" htmlFor="relativePath">
+                Use relative path
+              </label>
+            </div>
+            <div className="custom-control custom-checkbox custom-checkbox-info custom-control-inline">
+              <input
+                className="custom-control-input"
+                id="permanentLink"
+                type="checkbox"
+                checked={this.state.isUsePermanentLink}
+                onChange={this.toggleIsUsePamanentLink}
+                disabled={this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink}
+              />
+              <label className="custom-control-label" htmlFor="permanentLink">
+                Use permanent link
+              </label>
+            </div>
+          </div>
+          <div className="form-group row mb-0">
+            <label className="col-sm-3">Notation</label>
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="markdownType"
+                value={Linker.types.markdownLink}
+                checked={this.state.linkerType === Linker.types.markdownLink}
+                onChange={e => this.handleSelecteLinkerType(e.target.value)}
+              />
+              <label className="custom-control-label" htmlFor="markdownType">
+                Markdown
+              </label>
+            </div>
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="growiType"
+                value={Linker.types.growiLink}
+                checked={this.state.linkerType === Linker.types.growiLink}
+                onChange={e => this.handleSelecteLinkerType(e.target.value)}
+              />
+              <label className="custom-control-label" htmlFor="growiType">
+                Growi original
+              </label>
+            </div>
+            <div className="custom-control custom-radio custom-control-inline">
+              <input
+                type="radio"
+                className="custom-control-input"
+                id="pukiwikiType"
+                value={Linker.types.pukiwikiLink}
+                checked={this.state.linkerType === Linker.types.pukiwikiLink}
+                onChange={e => this.handleSelecteLinkerType(e.target.value)}
+              />
+              <label className="custom-control-label" htmlFor="pukiwikiType">
+                Pukiwiki
+              </label>
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+
   render() {
   render() {
     return (
     return (
-      <Modal isOpen={this.state.show} toggle={this.cancel} size="lg">
+      <Modal className="link-edit-modal" isOpen={this.state.show} toggle={this.cancel} size="lg">
         <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
         <ModalHeader tag="h4" toggle={this.cancel} className="bg-primary text-light">
           Edit Links
           Edit Links
         </ModalHeader>
         </ModalHeader>
 
 
         <ModalBody className="container">
         <ModalBody className="container">
           <div className="row">
           <div className="row">
-            <div className="col-12 col-lg-6">
-              <form className="form-group">
-                <div className="form-gorup my-3">
-                  <label htmlFor="linkInput">Link</label>
-                  <div className="input-group">
-                    <SearchTypeahead
-                      onChange={this.handleChangeTypeahead}
-                      onInputChange={this.handleChangeLinkInput}
-                      inputName="link"
-                      placeholder="Input page path or URL"
-                      keywordOnInit={this.state.linkInputValue}
-                    />
-                  </div>
-                </div>
-              </form>
-
-              <div className="d-block d-lg-none mb-3 overflow-auto">
-                {this.renderPreview()}
-              </div>
-
-              <div className="card">
-                <div className="card-body">
-                  <form className="form-group">
-                    <div className="form-group btn-group d-flex" role="group" aria-label="type">
-                      <button
-                        type="button"
-                        name={Linker.types.markdownLink}
-                        className={`btn btn-outline-secondary col ${this.state.linkerType === Linker.types.markdownLink && 'active'}`}
-                        onClick={e => this.handleSelecteLinkerType(e.target.name)}
-                      >
-                        Markdown
-                      </button>
-                      <button
-                        type="button"
-                        name={Linker.types.growiLink}
-                        className={`btn btn-outline-secondary col ${this.state.linkerType === Linker.types.growiLink && 'active'}`}
-                        onClick={e => this.handleSelecteLinkerType(e.target.name)}
-                      >
-                        Growi Original
-                      </button>
-                      {this.isApplyPukiwikiLikeLinkerPlugin && (
-                        <button
-                          type="button"
-                          name={Linker.types.pukiwikiLink}
-                          className={`btn btn-outline-secondary col ${this.state.linkerType === Linker.types.pukiwikiLink && 'active'}`}
-                          onClick={e => this.handleSelecteLinkerType(e.target.name)}
-                        >
-                          Pukiwiki
-                        </button>
-                      )}
-                    </div>
-
-                    <div className="form-group">
-                      <label htmlFor="label">Label</label>
-                      <input
-                        type="text"
-                        className="form-control"
-                        id="label"
-                        value={this.state.labelInputValue}
-                        onChange={e => this.handleChangeLabelInput(e.target.value)}
-                        disabled={this.state.linkerType === Linker.types.growiLink}
-                      />
-                    </div>
-                    <div className="form-inline">
-                      <div className="custom-control custom-checkbox custom-checkbox-info">
-                        <input
-                          className="custom-control-input"
-                          id="relativePath"
-                          type="checkbox"
-                          checked={this.state.isUseRelativePath}
-                          disabled={!this.state.linkInputValue.startsWith('/') || this.state.linkerType === Linker.types.growiLink}
-                        />
-                        <label className="custom-control-label" htmlFor="relativePath" onClick={this.toggleIsUseRelativePath}>
-                          Use relative path
-                        </label>
-                      </div>
-                    </div>
-                    <div className="form-inline">
-                      <div className="custom-control custom-checkbox custom-checkbox-info">
-                        <input
-                          className="custom-control-input"
-                          id="permanentLink"
-                          type="checkbox"
-                          checked={this.state.isUsePermanentLink}
-                          disabled={this.state.permalink === '' || this.state.linkerType === Linker.types.growiLink}
-                        />
-                        <label className="custom-control-label" htmlFor="permanentLink" onClick={this.toggleIsUsePamanentLink}>
-                          Use permanent link
-                        </label>
-                      </div>
-                    </div>
-                  </form>
-                </div>
-              </div>
+            <div className="col-12">
+              {this.renderLinkAndLabelForm()}
+              {this.renderPathFormatForm()}
             </div>
             </div>
-
-            <div className="col d-none d-lg-block pr-0 mr-3 overflow-auto">
-              {this.renderPreview()}
+          </div>
+          <div className="row">
+            <div className="col-12">
+              <h3 className="grw-modal-head">Preview</h3>
+              {this.renderLinkPreview()}
+            </div>
+          </div>
+          <div className="row">
+            <div className="col-12 text-center">
+              <button type="button" className="btn btn-sm btn-outline-secondary mx-1" onClick={this.hide}>
+                Cancel
+              </button>
+              <button type="submit" className="btn btn-sm btn-primary mx-1" onClick={this.save}>
+                Done
+              </button>
             </div>
             </div>
           </div>
           </div>
         </ModalBody>
         </ModalBody>
-        <ModalFooter>
-          <button type="button" className="btn btn-sm btn-outline-secondary" onClick={this.hide}>
-            Cancel
-          </button>
-          <button type="submit" className="btn btn-sm btn-primary" onClick={this.save}>
-            Done
-          </button>
-        </ModalFooter>
       </Modal>
       </Modal>
     );
     );
   }
   }

+ 3 - 4
src/client/js/components/PageEditor/MarkdownLinkUtil.js

@@ -27,16 +27,15 @@ class MarkdownLinkUtil {
   }
   }
 
 
   // replace link(link is an instance of Linker)
   // replace link(link is an instance of Linker)
-  replaceFocusedMarkdownLinkWithEditor(editor, link) {
+  replaceFocusedMarkdownLinkWithEditor(editor, linkText) {
     const curPos = editor.getCursor();
     const curPos = editor.getCursor();
-    const linkStr = link.generateMarkdownText();
     if (!this.isInLink(editor)) {
     if (!this.isInLink(editor)) {
-      editor.getDoc().replaceSelection(linkStr);
+      editor.getDoc().replaceSelection(linkText);
     }
     }
     else {
     else {
       const line = editor.getDoc().getLine(curPos.line);
       const line = editor.getDoc().getLine(curPos.line);
       const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(line, curPos.ch);
       const { beginningOfLink, endOfLink } = Linker.getBeginningAndEndIndexOfLink(line, curPos.ch);
-      editor.getDoc().replaceRange(linkStr, { line: curPos.line, ch: beginningOfLink }, { line: curPos.line, ch: endOfLink });
+      editor.getDoc().replaceRange(linkText, { line: curPos.line, ch: beginningOfLink }, { line: curPos.line, ch: endOfLink });
     }
     }
   }
   }
 
 

+ 8 - 10
src/client/js/components/PageHistory.jsx

@@ -53,21 +53,19 @@ function PageHistory(props) {
 
 
   function pager() {
   function pager() {
     return (
     return (
-      <div className="my-3">
-        <PaginationWrapper
-          activePage={pageHistoryContainer.state.activePage}
-          changePage={handlePage}
-          totalItemsCount={pageHistoryContainer.state.totalPages}
-          pagingLimit={pageHistoryContainer.state.pagingLimit}
-        />
-      </div>
+      <PaginationWrapper
+        activePage={pageHistoryContainer.state.activePage}
+        changePage={handlePage}
+        totalItemsCount={pageHistoryContainer.state.totalPages}
+        pagingLimit={pageHistoryContainer.state.pagingLimit}
+        align="center"
+      />
     );
     );
   }
   }
 
 
 
 
   return (
   return (
-    <div className="mt-4">
-      {pager()}
+    <div>
       <PageRevisionList
       <PageRevisionList
         revisions={pageHistoryContainer.state.revisions}
         revisions={pageHistoryContainer.state.revisions}
         diffOpened={pageHistoryContainer.state.diffOpened}
         diffOpened={pageHistoryContainer.state.diffOpened}

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

@@ -52,14 +52,14 @@ const PageList = (props) => {
   }
   }
 
 
   const pageList = pages.map(page => (
   const pageList = pages.map(page => (
-    <li key={page._id}>
+    <li key={page._id} className="mb-3">
       <Page page={page} />
       <Page page={page} />
     </li>
     </li>
   ));
   ));
 
 
   return (
   return (
     <div className="page-list-container-create">
     <div className="page-list-container-create">
-      <ul className="page-list-ul page-list-ul-flat mb-3">
+      <ul className="page-list-ul page-list-ul-flat ml-n4">
         {pageList}
         {pageList}
       </ul>
       </ul>
       <PaginationWrapper
       <PaginationWrapper
@@ -67,6 +67,7 @@ const PageList = (props) => {
         changePage={setPageNumber}
         changePage={setPageNumber}
         totalItemsCount={totalPages}
         totalItemsCount={totalPages}
         pagingLimit={limit}
         pagingLimit={limit}
+        align="center"
       />
       />
     </div>
     </div>
   );
   );

+ 31 - 0
src/client/js/components/PagePresentationModal.jsx

@@ -0,0 +1,31 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import {
+  Modal, ModalBody,
+} from 'reactstrap';
+
+const PagePresentationModal = (props) => {
+
+  function closeModalHandler() {
+    if (props.onClose === null) {
+      return;
+    }
+    props.onClose();
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={closeModalHandler} className="grw-presentation-modal" unmountOnClose={false}>
+      <ModalBody className="modal-body">
+        <iframe src={props.href} />
+      </ModalBody>
+    </Modal>
+  );
+};
+PagePresentationModal.propTypes = {
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func,
+  href: PropTypes.string.isRequired,
+};
+
+
+export default PagePresentationModal;

+ 22 - 20
src/client/js/components/PageTimeline.jsx

@@ -17,50 +17,51 @@ class PageTimeline extends React.Component {
     super(props);
     super(props);
 
 
     const { appContainer } = this.props;
     const { appContainer } = this.props;
-    this.showPages = this.showPages.bind(this);
-    this.handlePage = this.handlePage.bind(this);
     this.state = {
     this.state = {
       activePage: 1,
       activePage: 1,
-      totalPages: 0,
+      totalPageItems: 0,
       limit: appContainer.getConfig().recentCreatedLimit,
       limit: appContainer.getConfig().recentCreatedLimit,
 
 
       // TODO: remove after when timeline is implemented with React and inject data with props
       // TODO: remove after when timeline is implemented with React and inject data with props
       pages: this.props.pages,
       pages: this.props.pages,
     };
     };
 
 
+    this.handlePage = this.handlePage.bind(this);
   }
   }
 
 
-  async handlePage(selectedPage) {
-    await this.showPages(selectedPage);
-  }
 
 
-  async showPages(selectedPage) {
+  async handlePage(selectedPage) {
     const { appContainer, pageContainer } = this.props;
     const { appContainer, pageContainer } = this.props;
     const { path } = pageContainer.state;
     const { path } = pageContainer.state;
-    const limit = this.state.limit;
+    const { limit } = this.state;
     const offset = (selectedPage - 1) * limit;
     const offset = (selectedPage - 1) * limit;
-    const res = await appContainer.apiv3Get('/pages/list', { path, limit, offset });
     const activePage = selectedPage;
     const activePage = selectedPage;
-    const totalPages = res.data.totalCount;
+
+    const res = await appContainer.apiv3Get('/pages/list', { path, limit, offset });
+    const totalPageItems = res.data.totalCount;
     const pages = res.data.pages;
     const pages = res.data.pages;
     this.setState({
     this.setState({
       activePage,
       activePage,
-      totalPages,
+      totalPageItems,
       pages,
       pages,
     });
     });
   }
   }
 
 
   componentWillMount() {
   componentWillMount() {
     const { appContainer } = this.props;
     const { appContainer } = this.props;
-
     // initialize GrowiRenderer
     // initialize GrowiRenderer
     this.growiRenderer = appContainer.getRenderer('timeline');
     this.growiRenderer = appContainer.getRenderer('timeline');
-    this.showPages(1);
+  }
+
+  async componentDidMount() {
+    await this.handlePage(1);
+    this.setState({
+      activePage: 1,
+    });
   }
   }
 
 
   render() {
   render() {
     const { pages } = this.state;
     const { pages } = this.state;
-
     if (pages == null) {
     if (pages == null) {
       return <React.Fragment></React.Fragment>;
       return <React.Fragment></React.Fragment>;
     }
     }
@@ -87,8 +88,9 @@ class PageTimeline extends React.Component {
         <PaginationWrapper
         <PaginationWrapper
           activePage={this.state.activePage}
           activePage={this.state.activePage}
           changePage={this.handlePage}
           changePage={this.handlePage}
-          totalItemsCount={this.state.totalPages}
+          totalItemsCount={this.state.totalPageItems}
           pagingLimit={this.state.limit}
           pagingLimit={this.state.limit}
+          align="center"
         />
         />
       </div>
       </div>
     );
     );
@@ -97,6 +99,11 @@ class PageTimeline extends React.Component {
 
 
 }
 }
 
 
+/**
+ * Wrapper component for using unstated
+ */
+const PageTimelineWrapper = withUnstatedContainers(PageTimeline, [AppContainer, PageContainer]);
+
 PageTimeline.propTypes = {
 PageTimeline.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
@@ -104,9 +111,4 @@ PageTimeline.propTypes = {
   pages: PropTypes.arrayOf(PropTypes.object),
   pages: PropTypes.arrayOf(PropTypes.object),
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const PageTimelineWrapper = withUnstatedContainers(PageTimeline, [AppContainer, PageContainer]);
-
 export default withTranslation()(PageTimelineWrapper);
 export default withTranslation()(PageTimelineWrapper);

+ 19 - 1
src/client/js/components/PaginationWrapper.jsx

@@ -143,6 +143,20 @@ class PaginationWrapper extends React.Component {
 
 
   }
   }
 
 
+  getListClassName() {
+    const listClassNames = [];
+
+    const { align } = this.props;
+    if (align === 'center') {
+      listClassNames.push('justify-content-center');
+    }
+    if (align === 'right') {
+      listClassNames.push('justify-content-end');
+    }
+
+    return listClassNames.join(' ');
+  }
+
   render() {
   render() {
     const paginationItems = [];
     const paginationItems = [];
 
 
@@ -159,7 +173,7 @@ class PaginationWrapper extends React.Component {
 
 
     return (
     return (
       <React.Fragment>
       <React.Fragment>
-        <Pagination size="sm">{paginationItems}</Pagination>
+        <Pagination size="sm" listClassName={this.getListClassName()}>{paginationItems}</Pagination>
       </React.Fragment>
       </React.Fragment>
     );
     );
   }
   }
@@ -176,6 +190,10 @@ PaginationWrapper.propTypes = {
   changePage: PropTypes.func.isRequired,
   changePage: PropTypes.func.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
   totalItemsCount: PropTypes.number.isRequired,
   pagingLimit: PropTypes.number.isRequired,
   pagingLimit: PropTypes.number.isRequired,
+  align: PropTypes.string,
+};
+PaginationWrapper.defaultProps = {
+  align: 'left',
 };
 };
 
 
 export default withTranslation()(PaginationWrappered);
 export default withTranslation()(PaginationWrappered);

+ 3 - 6
src/client/js/components/ShareLink/ShareLink.jsx

@@ -85,8 +85,8 @@ class ShareLink extends React.Component {
     const { t } = this.props;
     const { t } = this.props;
 
 
     return (
     return (
-      <div className="container">
-        <h3 className="grw-modal-head  d-flex  pb-2">
+      <div className="container p-0">
+        <h3 className="grw-modal-head d-flex pb-2">
           { t('share_links.share_link_list') }
           { t('share_links.share_link_list') }
           <button className="btn btn-danger ml-auto " type="button" onClick={this.deleteAllLinksButtonHandler}>{t('delete_all')}</button>
           <button className="btn btn-danger ml-auto " type="button" onClick={this.deleteAllLinksButtonHandler}>{t('delete_all')}</button>
         </h3>
         </h3>
@@ -97,7 +97,7 @@ class ShareLink extends React.Component {
             onClickDeleteButton={this.deleteLinkById}
             onClickDeleteButton={this.deleteLinkById}
           />
           />
           <button
           <button
-            className="btn btn-outline-secondary d-block mx-auto px-5 mb-3"
+            className="btn btn-outline-secondary d-block mx-auto px-5"
             type="button"
             type="button"
             onClick={this.toggleShareLinkFormHandler}
             onClick={this.toggleShareLinkFormHandler}
           >
           >
@@ -120,9 +120,6 @@ ShareLink.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,
-
-  isOpen: PropTypes.bool.isRequired,
-  onClose: PropTypes.func.isRequired,
 };
 };
 
 
 export default withTranslation()(ShareLinkWrapper);
 export default withTranslation()(ShareLinkWrapper);

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

@@ -51,7 +51,7 @@ const TopOfTableContents = (props) => {
         <button
         <button
           type="button"
           type="button"
           className="btn btn-link grw-btn-top-of-table"
           className="btn btn-link grw-btn-top-of-table"
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('page-history')}
+          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('pageHistory')}
         >
         >
           <RecentChangesIcon />
           <RecentChangesIcon />
         </button>
         </button>
@@ -67,7 +67,7 @@ const TopOfTableContents = (props) => {
         <button
         <button
           type="button"
           type="button"
           className="btn btn-link grw-btn-top-of-table"
           className="btn btn-link grw-btn-top-of-table"
-          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('share-link')}
+          onClick={() => pageAccessoriesContainer.openPageAccessoriesModal('shareLink')}
         >
         >
           <ShareLinkIcon />
           <ShareLinkIcon />
         </button>
         </button>

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

@@ -197,29 +197,6 @@ $(() => {
         }
         }
       });
       });
     }
     }
-
-    // presentation
-    let presentaionInitialized = false;
-
-
-    const $b = $('body');
-
-    $(document).on('click', '.toggle-presentation', function(e) {
-      const $a = $(this);
-
-      e.preventDefault();
-      $b.toggleClass('overlay-on');
-
-      if (!presentaionInitialized) {
-        presentaionInitialized = true;
-
-        $('<iframe />').attr({
-          src: $a.attr('href'),
-        }).appendTo($('#presentation-container'));
-      }
-    }).on('click', '.fullscreen-layer', () => {
-      $b.toggleClass('overlay-on');
-    });
   } // end if pageId
   } // end if pageId
 
 
   // tab changing handling
   // tab changing handling

+ 7 - 26
src/client/js/models/Linker.js

@@ -1,17 +1,13 @@
 export default class Linker {
 export default class Linker {
 
 
   constructor(
   constructor(
-      type,
-      label,
-      link,
-      isUsePermanentLink = false,
-      permalink = '',
+      type = Linker.types.markdownLink,
+      label = '',
+      link = '',
   ) {
   ) {
     this.type = type;
     this.type = type;
     this.label = label;
     this.label = label;
     this.link = link;
     this.link = link;
-    this.isUsePermanentLink = isUsePermanentLink;
-    this.permalink = permalink;
 
 
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
     this.generateMarkdownText = this.generateMarkdownText.bind(this);
   }
   }
@@ -30,25 +26,15 @@ export default class Linker {
   }
   }
 
 
   generateMarkdownText() {
   generateMarkdownText() {
-    let reshapedLink = this.link;
-
-    if (this.isUsePermanentLink && this.permalink != null) {
-      reshapedLink = this.permalink;
-    }
-
-    if (this.label === '') {
-      this.label = reshapedLink;
-    }
-
     if (this.type === Linker.types.pukiwikiLink) {
     if (this.type === Linker.types.pukiwikiLink) {
-      if (this.label === reshapedLink) return `[[${reshapedLink}]]`;
-      return `[[${this.label}>${reshapedLink}]]`;
+      if (this.label === this.link) return `[[${this.link}]]`;
+      return `[[${this.label}>${this.link}]]`;
     }
     }
     if (this.type === Linker.types.growiLink) {
     if (this.type === Linker.types.growiLink) {
-      return `[${reshapedLink}]`;
+      return `[${this.link}]`;
     }
     }
     if (this.type === Linker.types.markdownLink) {
     if (this.type === Linker.types.markdownLink) {
-      return `[${this.label}](${reshapedLink})`;
+      return `[${this.label}](${this.link})`;
     }
     }
   }
   }
 
 
@@ -82,15 +68,10 @@ export default class Linker {
       link = label;
       link = label;
     }
     }
 
 
-    const isUsePermanentLink = false;
-    const permalink = '';
-
     return new Linker(
     return new Linker(
       type,
       type,
       label,
       label,
       link,
       link,
-      isUsePermanentLink,
-      permalink,
     );
     );
   }
   }
 
 

+ 4 - 19
src/client/js/services/AdminCustomizeContainer.js

@@ -24,7 +24,6 @@ export default class AdminCustomizeContainer extends Container {
       retrieveError: null,
       retrieveError: null,
       // set dummy value tile for using suspense
       // set dummy value tile for using suspense
       currentTheme: this.dummyCurrentTheme,
       currentTheme: this.dummyCurrentTheme,
-      currentLayout: '',
       isEnabledTimeline: false,
       isEnabledTimeline: false,
       isSavedStatesOfTabChanges: false,
       isSavedStatesOfTabChanges: false,
       isEnabledAttachTitleHeader: false,
       isEnabledAttachTitleHeader: false,
@@ -72,7 +71,6 @@ export default class AdminCustomizeContainer extends Container {
 
 
       this.setState({
       this.setState({
         currentTheme: customizeParams.themeType,
         currentTheme: customizeParams.themeType,
-        currentLayout: customizeParams.layoutType,
         isEnabledTimeline: customizeParams.isEnabledTimeline,
         isEnabledTimeline: customizeParams.isEnabledTimeline,
         isSavedStatesOfTabChanges: customizeParams.isSavedStatesOfTabChanges,
         isSavedStatesOfTabChanges: customizeParams.isSavedStatesOfTabChanges,
         isEnabledAttachTitleHeader: customizeParams.isEnabledAttachTitleHeader,
         isEnabledAttachTitleHeader: customizeParams.isEnabledAttachTitleHeader,
@@ -97,21 +95,10 @@ export default class AdminCustomizeContainer extends Container {
     }
     }
   }
   }
 
 
-  /**
-   * Switch layoutType
-   */
-  switchLayoutType(lauoutName) {
-    this.setState({ currentLayout: lauoutName });
-  }
-
   /**
   /**
    * Switch themeType
    * Switch themeType
    */
    */
   switchThemeType(themeName) {
   switchThemeType(themeName) {
-    // can't choose theme when kibela
-    if (this.state.currentLayout === 'kibela') {
-      return;
-    }
     this.setState({ currentTheme: themeName });
     this.setState({ currentTheme: themeName });
 
 
     // preview if production
     // preview if production
@@ -216,7 +203,7 @@ export default class AdminCustomizeContainer extends Container {
   async previewTheme(themeName) {
   async previewTheme(themeName) {
     try {
     try {
       // get theme asset path
       // get theme asset path
-      const response = await this.appContainer.apiv3.get('/customize-setting/layout-theme/asset-path', { themeName });
+      const response = await this.appContainer.apiv3.get('/customize-setting/theme/asset-path', { themeName });
       const { assetPath } = response.data;
       const { assetPath } = response.data;
 
 
       const themeLink = document.getElementById('grw-theme-link');
       const themeLink = document.getElementById('grw-theme-link');
@@ -239,18 +226,16 @@ export default class AdminCustomizeContainer extends Container {
   }
   }
 
 
   /**
   /**
-   * Update layout
+   * Update theme
    * @memberOf AdminCustomizeContainer
    * @memberOf AdminCustomizeContainer
    */
    */
-  async updateCustomizeLayoutAndTheme() {
+  async updateCustomizeTheme() {
     try {
     try {
-      const response = await this.appContainer.apiv3.put('/customize-setting/layout-theme', {
-        layoutType: this.state.currentLayout,
+      const response = await this.appContainer.apiv3.put('/customize-setting/theme', {
         themeType: this.state.currentTheme,
         themeType: this.state.currentTheme,
       });
       });
       const { customizedParams } = response.data;
       const { customizedParams } = response.data;
       this.setState({
       this.setState({
-        layoutType: customizedParams.layoutType,
         themeType: customizedParams.themeType,
         themeType: customizedParams.themeType,
       });
       });
     }
     }

+ 1 - 0
src/client/js/services/PageAccessoriesContainer.js

@@ -41,6 +41,7 @@ export default class PageAccessoriesContainer extends Container {
   closePageAccessoriesModal() {
   closePageAccessoriesModal() {
     this.setState({
     this.setState({
       isPageAccessoriesModalShown: false,
       isPageAccessoriesModalShown: false,
+      activeTab: '',
     });
     });
   }
   }
 
 

+ 0 - 169
src/client/styles/scss/_comment_kibela.scss

@@ -1,169 +0,0 @@
-.kibela {
-  /* Comment section */
-  %comment-section {
-    position: relative;
-    padding: 1em;
-
-    // speech balloon
-    &:before {
-      position: absolute;
-      top: 1.5em;
-      left: -1em;
-      display: block;
-      width: 0;
-      height: 0;
-      content: '';
-      border-top: 20px solid transparent;
-      border-right: 20px solid $gray-200;
-      border-bottom: 20px solid transparent;
-      border-left: 20px solid transparent;
-      border-left-width: 0;
-
-      @include media-breakpoint-down(xs) {
-        top: 1em;
-      }
-    }
-  }
-
-  %picture {
-    float: left;
-    width: 3em;
-    height: 3em;
-    margin-top: 0.8em;
-
-    @include media-breakpoint-down(xs) {
-      width: 2em;
-      height: 2em;
-    }
-  }
-
-  .page-comments-row {
-    margin: 10px 0px;
-  }
-
-  .page-comments {
-    h4 {
-      margin-bottom: 1em;
-    }
-  }
-  .page-comment {
-    position: relative;
-
-    // ユーザー名
-    .page-comment-creator {
-      margin-top: -0.5em;
-      margin-bottom: 0.5em;
-      font-weight: bold;
-    }
-
-    // ユーザーアイコン
-    .picture {
-      @extend %picture;
-    }
-
-    // コメントセクション
-    .page-comment-main {
-      @extend %comment-section;
-      margin-left: 4.5em;
-      background: $gray-200;
-      border-radius: 0.35em;
-    }
-
-    // コメント本文
-    .page-comment-body {
-      margin-bottom: 0.5em;
-      word-wrap: break-word;
-    }
-  }
-
-  /*
-   * reply
-   */
-  .page-comment-reply {
-    margin-top: 1em;
-  }
-  // remove margin after hidden replies
-  .page-comments-hidden-replies + .page-comment-reply {
-    margin-top: 0;
-  }
-  .page-comment-reply,
-  .page-comment-reply-form {
-    margin-right: 15px;
-    margin-left: 6em;
-  }
-  // reply button
-  .btn.btn-comment-reply {
-    width: 120px;
-    margin-top: 0.5em;
-    margin-right: 15px;
-
-    border-top: none;
-    border-right: none;
-    border-left: none;
-  }
-
-  // display cheatsheet for comment form only
-  .comment-form {
-    .editor-cheatsheet {
-      display: none;
-    }
-
-    position: relative;
-    margin-top: 1em;
-
-    // user icon
-    .picture {
-      @extend %picture;
-    }
-
-    // seciton
-    .comment-form-main {
-      @extend %comment-section;
-      margin-left: 4.5em;
-      @include media-breakpoint-down(xs) {
-        margin-left: 3.5em;
-      }
-      background: #e6e9ec;
-      border-radius: 0.35em;
-      .CodeMirror {
-        border: 0px;
-      }
-    }
-
-    // textarea
-    .comment-write {
-      margin-bottom: 0.5em;
-    }
-    .comment-form-comment {
-      height: 80px;
-      &:focus,
-      &:not(:invalid) {
-        height: 180px;
-        transition: height 0.2s ease-out;
-      }
-    }
-    .CodeMirror {
-      border: 0px !important;
-    }
-
-    //// TODO: migrate to Bootstrap 4
-    // use @include media-breakpoint-*
-    // #page-editor {
-    //   @media (max-width: $screen-sm) {
-    //     .desc-long {
-    //       display: none;
-    //     }
-    //   }
-    // }
-    // @media screen and (max-width: 1400px) {
-    //   .desc-long {
-    //     display: none;
-    //   }
-    //   @media screen and (max-width: 570px) {
-    //     .gfm-cheatsheet {
-    //       display: none;
-    //     }
-    //   }
-    // }
-  }
-}

+ 0 - 170
src/client/styles/scss/_layout_kibela.scss

@@ -1,170 +0,0 @@
-$navbar-height-adjustment: 10px;
-
-body.kibela {
-  .grw-pt-10px {
-    padding-top: 10px !important;
-  }
-
-  /* Logo */
-  .logo {
-    .logo-mark {
-      height: 50px;
-      box-shadow: none;
-
-      svg {
-        width: 60px;
-      }
-    }
-  }
-
-  /* header */
-  .authors {
-    padding-top: 10px;
-
-    li {
-      list-style: none !important;
-    }
-  }
-
-  .panel-heading {
-    border-radius: 0 !important;
-  }
-
-  /* page list */
-  .page-attachments-row {
-    border: 0px;
-  }
-
-  .round-corner {
-    border-radius: 0.35em;
-  }
-
-  .round-corner-top {
-    z-index: absolute;
-    border-radius: 0.35em;
-  }
-
-  .kibela-block {
-    position: relative;
-    top: 30px;
-    right: 100px;
-    bottom: 0px;
-    left: 0px;
-    z-index: absolute;
-    max-width: 1024px;
-    min-height: 8em;
-    margin: auto;
-    border-radius: 0.35em;
-    @include media-breakpoint-down(xs) {
-      top: 0px;
-    }
-  }
-
-  .grw-subnav {
-    position: relative;
-    border: none;
-
-    svg {
-      display: none;
-    }
-
-    &.grw-subnav-user-page {
-      min-height: 128px;
-    }
-
-    @media screen and (max-width: 765px) {
-      padding-top: 30px;
-    }
-
-    @include media-breakpoint-down(xs) {
-      padding-top: 0px;
-    }
-  }
-
-  .revision-toc {
-    position: sticky;
-    top: calc(60px + 5px);
-    right: 10rem;
-    min-width: 260px;
-    margin-top: 40px;
-
-    .revision-toc-content {
-      padding: 0;
-    }
-
-    @media screen and (max-width: 1400px) {
-      &.affix {
-        right: 0rem !important;
-        transition: 0.5s;
-      }
-    }
-  }
-
-  /* admin navigation */
-  .admin-navigation {
-    .list-group-item + .list-group-item.active {
-      margin-top: 2px;
-    }
-  }
-
-  /* Tabs */
-  .nav.nav-tabs {
-    > .nav-item {
-      cursor: pointer;
-
-      > .nav-link {
-        border: none;
-        border-radius: 3px;
-      }
-    }
-  }
-
-  /* edit */
-  .CodeMirror {
-    border-radius: 0.35em;
-  }
-
-  &.on-edit {
-    $header-plus-footer: 42px //  .nav height
-      + 5.5px //                  .kibela-block border-top
-      + 15px //                   .tab-content padding-top
-      + 1px //                    .page-editor-footer border-top
-      + 60px; //                  .page-editor-footer min-height
-
-    @include expand-editor($header-plus-footer, $navbar-height-adjustment);
-
-    .kibela-block {
-      top: 0px;
-      max-width: unset;
-      padding-top: 0px;
-      border: 0px;
-    }
-
-    .tab-content {
-      padding-top: 15px;
-
-      #edit {
-        margin-right: 1em;
-        margin-left: 1em;
-      }
-    }
-
-    .tab-pane {
-      .page-editor-editor-container {
-        margin: 0px;
-        border: none !important;
-      }
-    }
-
-    .page-editor-preview-container {
-      padding-right: 0px !important;
-      padding-left: 2em;
-    }
-
-    .page-editor-footer {
-      min-height: 60px;
-      padding: 13px;
-      margin: 0;
-    }
-  }
-}

+ 13 - 5
src/client/styles/scss/_linkedit-preview.scss

@@ -1,8 +1,16 @@
-.modal .modal-body .linkedit-preview {
-  height: 0;
-  padding-bottom: 50%;
-
+.linkedit-preview {
   .page-editor-preview-body {
   .page-editor-preview-body {
-    overflow-y: unset;
+    max-height: 70vh;
+    padding-top: 0px;
+    margin: 0px -10px 0px -10px;
+    .wiki {
+      overflow-y: scroll;
+    }
   }
   }
 }
 }
+
+// page preview button
+.btn-page-preview svg {
+  width: 18px;
+  height: 18px;
+}

+ 0 - 36
src/client/styles/scss/_navbar_kibela.scss

@@ -1,36 +0,0 @@
-/* navbar */
-
-.kibela {
-  .grw-navbar {
-    height: 60px;
-    background: white;
-    border-bottom: solid 1px $gray-200;
-    .navbar-nav {
-      .confidential {
-        color: white;
-        background: #0d3e75;
-      }
-      & > li > a {
-        height: 40px !important;
-        margin-right: 1.5em;
-        color: #3c4a60;
-        border-radius: 0.35em;
-        &:hover {
-          color: #3c4a60;
-        }
-      }
-    }
-
-    .btn-create-page {
-      background: #5584e1;
-      border-radius: 0.35em;
-      &:hover {
-        background: rgb(124, 168, 255);
-      }
-      span,
-      i {
-        color: white;
-      }
-    }
-  }
-}

+ 13 - 0
src/client/styles/scss/_page-presentation.scss

@@ -0,0 +1,13 @@
+.grw-presentation-modal {
+  @include expand-modal-fullscreen(false, false);
+
+  .modal-body {
+    background: black;
+
+    iframe {
+      width: 100%;
+      height: 100%;
+      border: 0;
+    }
+  }
+}

+ 0 - 45
src/client/styles/scss/_page.scss

@@ -135,51 +135,6 @@
   }
   }
 }
 }
 
 
-/*
- * for Presentation
- */
-.fullscreen-layer {
-  position: fixed;
-  top: 0;
-  left: 0;
-  z-index: 9999;
-  width: 100%;
-  height: 0;
-  background: rgba(0, 0, 0, 0.5);
-  opacity: 0;
-  transition: opacity 0.3s ease-out;
-
-  & > * {
-    box-shadow: 0 0 20px rgba(0, 0, 0, 0.8);
-  }
-}
-
-.overlay-on {
-  #wrapper {
-    filter: blur(5px);
-  }
-
-  .fullscreen-layer {
-    height: 100%;
-    opacity: 1;
-  }
-}
-
-#presentation-container {
-  position: absolute;
-  top: 5%;
-  left: 5%;
-  width: 90%;
-  height: 90%;
-  background: black;
-
-  iframe {
-    width: 100%;
-    height: 100%;
-    border: 0;
-  }
-}
-
 .card.grw-page-status-alert {
 .card.grw-page-status-alert {
   $margin-bottom: $grw-navbar-bottom-height + 10px;
   $margin-bottom: $grw-navbar-bottom-height + 10px;
 
 

+ 35 - 2
src/client/styles/scss/_page_accessaries_modal.scss

@@ -1,7 +1,40 @@
 .grw-page-accessories-modal {
 .grw-page-accessories-modal {
+  .nav-title {
+    flex-wrap: nowrap;
+
+    li {
+      a.nav-link {
+        padding: 1rem 1.5rem;
+      }
+    }
+  }
+  .modal-header {
+    button.close {
+      margin: auto 0rem auto auto;
+    }
+  }
+
+  .grw-nav-slide-hr {
+    border-top: 0rem;
+    border-bottom: 3px solid;
+    transition: 0.3s ease-in-out;
+  }
   .nav-link svg {
   .nav-link svg {
-    width: 20px;
-    height: 20px;
+    width: 17px;
+    height: 17px;
     margin-right: 5px;
     margin-right: 5px;
   }
   }
+
+  .grw-modal-body-style {
+    max-height: calc(100vh - 100px);
+  }
+  ul.pagination {
+    margin-bottom: 0rem;
+  }
+}
+
+// revision-history
+// to stay d2h-code-side-line-number in the revision history diff area
+.d2h-wrapper {
+  position: relative;
 }
 }

+ 1 - 4
src/client/styles/scss/style-app.scss

@@ -29,9 +29,7 @@
 @import 'attachments';
 @import 'attachments';
 @import 'comment';
 @import 'comment';
 @import 'comment_growi';
 @import 'comment_growi';
-@import 'comment_kibela';
 @import 'drawio';
 @import 'drawio';
-@import 'navbar_kibela';
 @import 'create-page';
 @import 'create-page';
 @import 'draft';
 @import 'draft';
 @import 'editor-attachment';
 @import 'editor-attachment';
@@ -39,16 +37,15 @@
 @import 'handsontable';
 @import 'handsontable';
 @import 'layout';
 @import 'layout';
 @import 'layout_growi';
 @import 'layout_growi';
-@import 'layout_kibela';
 @import 'login';
 @import 'login';
 @import 'me';
 @import 'me';
 @import 'mirror_mode';
 @import 'mirror_mode';
 @import 'navbar';
 @import 'navbar';
-@import 'navbar_kibela';
 @import 'on-edit';
 @import 'on-edit';
 @import 'page_list';
 @import 'page_list';
 @import 'page-path';
 @import 'page-path';
 @import 'page';
 @import 'page';
+@import 'page-presentation';
 @import 'search';
 @import 'search';
 @import 'shortcuts';
 @import 'shortcuts';
 @import 'sidebar';
 @import 'sidebar';

+ 40 - 1
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -57,6 +57,10 @@ textarea.form-control {
   border-right: none;
   border-right: none;
 }
 }
 
 
+.input-group input {
+  border-color: $secondary;
+}
+
 /*
 /*
  * Dropdown
  * Dropdown
  */
  */
@@ -81,10 +85,20 @@ textarea.form-control {
 /*
 /*
  * Card
  * Card
  */
  */
-.card:not([class*='bg-']) {
+.card:not([class*='bg-']):not(.well):not(.card-disabled) {
   @extend .bg-dark;
   @extend .bg-dark;
 }
 }
 
 
+// [TODO] GW-3219 modify common color of well in dark theme, then remove below css.
+.card.well {
+  border-color: $secondary;
+}
+
+.card.card-disabled {
+  background-color: lighten($dark, 10%);
+  border-color: $secondary;
+}
+
 /*
 /*
  * Pagination
  * Pagination
  */
  */
@@ -209,6 +223,15 @@ ul.pagination {
   background-color: rgba($bgcolor-subnav, 0.85);
   background-color: rgba($bgcolor-subnav, 0.85);
 }
 }
 
 
+.grw-three-stranded-button {
+  .btn-outline-primary {
+    &:hover {
+      color: $primary;
+      background-color: $gray-700;
+    }
+  }
+}
+
 // Search drop down
 // Search drop down
 #search-typeahead-asynctypeahead {
 #search-typeahead-asynctypeahead {
   background-color: $bgcolor-global;
   background-color: $bgcolor-global;
@@ -237,6 +260,22 @@ body.on-edit {
   }
   }
 }
 }
 
 
+/*
+ * Popover
+ */
+.popover {
+  background-color: $bgcolor-global;
+  border-color: $secondary;
+  .popover-header {
+    color: $white;
+    background-color: $secondary;
+    border-color: $secondary;
+  }
+  .popover-body {
+    color: inherit;
+  }
+}
+
 /*
 /*
  * GROWI HandsontableModal
  * GROWI HandsontableModal
  */
  */

+ 0 - 186
src/client/styles/scss/theme/_apply-colors-kibela.scss

@@ -1,186 +0,0 @@
-body.kibela {
-  .growi:not(.login-page) {
-    .icon-link,
-    .CodeMirror-hint-active,
-    .grw-nav-main-left-tab,
-    .tav-pane,
-    .active {
-      color: $subthemecolor;
-    }
-
-    .bg-white {
-      background: #fefffe !important;
-    }
-
-    .bg-primary {
-      background-color: $primary !important;
-    }
-
-    .grw-subnav {
-      background-color: rgba(lighten($bgcolor-global, 50%), 1);
-    }
-
-    .grw-subnav-fixed-container .grw-subnav {
-      background-color: rgba(lighten($bgcolor-global, 50%), 0.85);
-    }
-
-    /* page wrapper */
-    #page-wrapper {
-      background-color: $bgcolor-global;
-    }
-
-    .search-input-group,
-    .search-typeahead {
-      .btn {
-        background-color: transparent;
-      }
-    }
-
-    .btn-open-dropzone {
-      background: $themelight;
-    }
-
-    /* page list */
-    .page-list {
-      background: white;
-    }
-
-    .page-attachments-row {
-      background-color: #e5ecf1;
-    }
-
-    /* round */
-    .round-corner-top {
-      border-top: solid 0.4em $thickborder;
-    }
-
-    /* admin navigation */
-    .admin-navigation {
-      .list-group-item {
-        background-color: transparent;
-
-        &:hover {
-          background: $gray-100;
-        }
-      }
-
-      .list-group-item.active {
-        color: white;
-        background: $bgcolor-navbar-active;
-      }
-    }
-
-    /* search page */
-    .search-result-list,
-    .page-list-li {
-      background: $themelight;
-    }
-
-    /* Tabs */
-    .nav.nav-tabs {
-      > .nav-item {
-        color: $color-link;
-        background: transparent;
-
-        &:hover,
-        &:focus {
-          > .nav-link {
-            color: $color-link-hover;
-          }
-        }
-
-        > .nav-link {
-          color: $color-link;
-        }
-
-        > .nav-link.active {
-          background: transparent !important;
-          border-bottom: solid 2.7px $thickborder;
-        }
-      }
-    }
-
-    /* wiki */
-    .wiki {
-      h1 {
-        border-bottom: solid 2px $thickborder !important;
-      }
-
-      h2 {
-        border-color: solid 1px $thickborder;
-      }
-
-      // change color of highlighted header in wiki (default: orange)
-      .code-line.revision-head.highlighted {
-        color: $themelight;
-        background-color: lighten($bgcolor-theme, 20%);
-
-        .icon-note,
-        .icon-link {
-          color: $themelight;
-        }
-      }
-    }
-
-    /* Modal */
-    .modal-title {
-      color: white; // override header colors
-    }
-    .modal-content {
-      background-color: $themelight;
-    }
-
-    /* Inline Code */
-    :not(.hljs) > code:not(.hljs) {
-      color: $color-inline-code;
-      background-color: $bgcolor-inline-code;
-      border: solid 1px $bordercolor-inline-code;
-      border-radius: 0.35em;
-    }
-
-    /* button */
-    .btn-primary {
-      background: $primary;
-      border: 1px solid $primary;
-    }
-
-    /* edit */
-    .CodeMirror {
-      border: solid 1.2px #d8d8d8;
-      border-top: solid 0.3em $thickborder !important;
-    }
-
-    &.on-edit {
-      .page-editor-preview-container {
-        background: white !important;
-      }
-    }
-
-    /* navbar */
-    .grw-navbar {
-      .nav-item > .nav-link {
-        &:hover {
-          color: $color-link-nabvar-hover;
-        }
-        &:focus {
-          color: $color-link-nabvar;
-        }
-      }
-      #personal-dropdown {
-        a.nav-link {
-          color: $color-global;
-        }
-      }
-    }
-
-    /* h */
-    h1,
-    h2,
-    h3,
-    h4,
-    h5,
-    h6 {
-      color: $color-header;
-    }
-  }
-}

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

@@ -42,6 +42,14 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: darken($bgcolor-global, 5%);
   background-color: darken($bgcolor-global, 5%);
 }
 }
 
 
+/*
+ * card
+ */
+.card.card-disabled {
+  background-color: darken($bgcolor-card, 5%);
+  border-color: $gray-200;
+}
+
 /*
 /*
  * GROWI Login form
  * GROWI Login form
  */
  */
@@ -132,6 +140,15 @@ $table-hover-bg: $bgcolor-table-hover;
   background-color: rgba($bgcolor-subnav, 0.85);
   background-color: rgba($bgcolor-subnav, 0.85);
 }
 }
 
 
+.grw-three-stranded-button {
+  .btn-outline-primary {
+    &:hover {
+      color: $primary;
+      background-color: $gray-200;
+    }
+  }
+}
+
 .grw-drawer-toggler {
 .grw-drawer-toggler {
   @extend .btn-light;
   @extend .btn-light;
   color: $gray-500;
   color: $gray-500;
@@ -177,6 +194,15 @@ $table-hover-bg: $bgcolor-table-hover;
   }
   }
 }
 }
 
 
+/*
+ * GROWI Link Edit Modal
+ */
+.link-edit-modal {
+  span i {
+    color: $gray-400;
+  }
+}
+
 /*
 /*
  * GROWI HandsontableModal
  * GROWI HandsontableModal
  */
  */

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

@@ -265,6 +265,11 @@ ul.pagination {
   fill: $color-editor-icons;
   fill: $color-editor-icons;
 }
 }
 
 
+// page preview button in link form
+.btn-page-preview svg {
+  fill: white;
+}
+
 /*
 /*
  * Modal
  * Modal
  */
  */
@@ -314,11 +319,20 @@ ul.pagination {
   .nav-title {
   .nav-title {
     color: $color-link;
     color: $color-link;
   }
   }
+  .modal-header {
+    button.close {
+      color: $secondary;
+    }
+  }
   .nav-link svg {
   .nav-link svg {
     fill: $color-link;
     fill: $color-link;
   }
   }
-  .active-border {
-    border-bottom: 2px solid $color-link;
+  .modal-split-hr {
+    background-color: $bordercolor-nav-tabs;
+  }
+
+  .grw-nav-slide-hr {
+    border-color: $color-link;
   }
   }
 }
 }
 
 

+ 30 - 1
src/client/styles/scss/theme/kibela.scss

@@ -6,6 +6,36 @@ $themelight: #f4f5f6;
 $subthemecolor: rgb(88, 130, 250);
 $subthemecolor: rgb(88, 130, 250);
 $lightthemecolor: rgba(181, 203, 247, 0.61);
 $lightthemecolor: rgba(181, 203, 247, 0.61);
 
 
+// change width only for pages with articles
+.growi:not(.on-edit):not(.admin-page):not(.user-settings-page) {
+  // layout
+  header,
+  #main {
+    max-width: 1024px;
+    margin: auto;
+  }
+  header {
+    margin-top: 30px;
+    margin-bottom: 42px;
+    background-color: $gray-100;
+  }
+}
+
+.grw-subnav {
+  padding: 20px 30px;
+  border-radius: 0.35em;
+}
+
+.grw-page-content-container {
+  padding-top: 10px;
+  background-color: #fff;
+  border-radius: 0.35em;
+}
+
+.page-attachments-row {
+  margin-top: 30px;
+}
+
 // Light Mode
 // Light Mode
 html[light],
 html[light],
 html[dark] {
 html[dark] {
@@ -78,5 +108,4 @@ html[dark] {
 
 
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-light';
   @import 'apply-colors-light';
-  @import 'apply-colors-kibela';
 }
 }

+ 25 - 3
src/server/routes/apiv3/attachment.js

@@ -5,6 +5,7 @@ const logger = loggerFactory('growi:routes:apiv3:attachment'); // eslint-disable
 const express = require('express');
 const express = require('express');
 
 
 const router = express.Router();
 const router = express.Router();
+const { query } = require('express-validator');
 
 
 const ErrorV3 = require('../../models/vo/error-apiv3');
 const ErrorV3 = require('../../models/vo/error-apiv3');
 
 
@@ -18,8 +19,18 @@ 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);
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
+  const User = crowi.model('User');
   const Attachment = crowi.model('Attachment');
   const Attachment = crowi.model('Attachment');
+  const apiV3FormValidator = require('../../middlewares/apiv3-form-validator')(crowi);
 
 
+
+  const validator = {
+    retrieveAttachments: [
+      query('pageId').isMongoId().withMessage('pageId is required'),
+      query('limit').isInt({ min: 1 }),
+      query('offset').isInt({ min: 0 }),
+    ],
+  };
   /**
   /**
    * @swagger
    * @swagger
    *
    *
@@ -38,10 +49,9 @@ module.exports = (crowi) => {
    *            schema:
    *            schema:
    *              type: string
    *              type: string
    */
    */
-  router.get('/list', accessTokenParser, loginRequired, async(req, res) => {
+  router.get('/list', accessTokenParser, loginRequired, validator.retrieveAttachments, apiV3FormValidator, async(req, res) => {
     const offset = +req.query.offset || 0;
     const offset = +req.query.offset || 0;
     const limit = +req.query.limit || 30;
     const limit = +req.query.limit || 30;
-    const queryOptions = { offset, limit };
 
 
     try {
     try {
       const pageId = req.query.pageId;
       const pageId = req.query.pageId;
@@ -54,8 +64,20 @@ module.exports = (crowi) => {
 
 
       const paginateResult = await Attachment.paginate(
       const paginateResult = await Attachment.paginate(
         { page: pageId },
         { page: pageId },
-        queryOptions,
+        {
+          limit,
+          offset,
+          populate: {
+            path: 'creator',
+            select: User.USER_PUBLIC_FIELDS,
+          },
+        },
       );
       );
+      paginateResult.docs.forEach((doc) => {
+        if (doc.creator != null && doc.creator instanceof User) {
+          doc.creator = doc.creator.toObject();
+        }
+      });
 
 
       return res.apiv3({ paginateResult });
       return res.apiv3({ paginateResult });
     }
     }

+ 21 - 26
src/server/routes/apiv3/customize-setting.js

@@ -21,12 +21,10 @@ const ErrorV3 = require('../../models/vo/error-apiv3');
  *
  *
  *  components:
  *  components:
  *    schemas:
  *    schemas:
- *      CustomizeLayoutTheme:
- *        description: CustomizeLayoutTheme
+ *      CustomizeTheme:
+ *        description: CustomizeTheme
  *        type: object
  *        type: object
  *        properties:
  *        properties:
- *          layoutType:
- *            type: string
  *          themeType:
  *          themeType:
  *            type: string
  *            type: string
  *      CustomizeFunction:
  *      CustomizeFunction:
@@ -92,10 +90,9 @@ module.exports = (crowi) => {
         'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'future', 'halloween', 'spring',
         'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'future', 'halloween', 'spring',
       ]),
       ]),
     ],
     ],
-    layoutTheme: [
-      body('layoutType').isString().isIn(['growi', 'kibela']),
+    theme: [
       body('themeType').isString().isIn([
       body('themeType').isString().isIn([
-        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'future', 'halloween', 'spring',
+        'default', 'nature', 'mono-blue', 'wood', 'island', 'christmas', 'antarctic', 'future', 'halloween', 'spring', 'kibela',
       ]),
       ]),
     ],
     ],
     function: [
     function: [
@@ -171,12 +168,12 @@ module.exports = (crowi) => {
   /**
   /**
    * @swagger
    * @swagger
    *
    *
-   *    /customize-setting/layout-theme/asset-path:
+   *    /customize-setting/theme/asset-path:
    *      put:
    *      put:
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
-   *        operationId: getLayoutThemeAssetPath
-   *        summary: /customize-setting/layout-theme/asset-path
-   *        description: Get layout theme asset path
+   *        operationId: getThemeAssetPath
+   *        summary: /customize-setting/theme/asset-path
+   *        description: Get theme asset path
    *        parameters:
    *        parameters:
    *          - name: themeName
    *          - name: themeName
    *            in: query
    *            in: query
@@ -185,7 +182,7 @@ module.exports = (crowi) => {
    *              type: string
    *              type: string
    *        responses:
    *        responses:
    *          200:
    *          200:
-   *            description: Succeeded to update layout and theme
+   *            description: Succeeded to get theme asset path
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
@@ -193,8 +190,8 @@ module.exports = (crowi) => {
    *                    assetPath:
    *                    assetPath:
    *                      type: string
    *                      type: string
    */
    */
-  router.get('/layout-theme/asset-path', loginRequiredStrictly, adminRequired, validator.themeAssetPath, apiV3FormValidator, async(req, res) => {
-    const themeName = req.query.themeName;
+  router.get('/theme/asset-path', loginRequiredStrictly, adminRequired, validator.themeAssetPath, apiV3FormValidator, async(req, res) => {
+    const { themeName } = req.query;
 
 
     const webpackAssetKey = `styles/theme-${themeName}.css`;
     const webpackAssetKey = `styles/theme-${themeName}.css`;
     const assetPath = res.locals.webpack_asset(webpackAssetKey);
     const assetPath = res.locals.webpack_asset(webpackAssetKey);
@@ -209,44 +206,42 @@ module.exports = (crowi) => {
   /**
   /**
    * @swagger
    * @swagger
    *
    *
-   *    /customize-setting/layout-theme:
+   *    /customize-setting/theme:
    *      put:
    *      put:
    *        tags: [CustomizeSetting]
    *        tags: [CustomizeSetting]
-   *        operationId: updateLayoutThemeCustomizeSetting
-   *        summary: /customize-setting/layout-theme
-   *        description: Update layout and theme
+   *        operationId: updateThemeCustomizeSetting
+   *        summary: /customize-setting/theme
+   *        description: Update theme
    *        requestBody:
    *        requestBody:
    *          required: true
    *          required: true
    *          content:
    *          content:
    *            application/json:
    *            application/json:
    *              schema:
    *              schema:
-   *                $ref: '#/components/schemas/CustomizeLayoutTheme'
+   *                $ref: '#/components/schemas/CustomizeTheme'
    *        responses:
    *        responses:
    *          200:
    *          200:
-   *            description: Succeeded to update layout and theme
+   *            description: Succeeded to update theme
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  $ref: '#/components/schemas/CustomizeLayoutTheme'
+   *                  $ref: '#/components/schemas/CustomizeTheme'
    */
    */
-  router.put('/layout-theme', loginRequiredStrictly, adminRequired, csrf, validator.layoutTheme, apiV3FormValidator, async(req, res) => {
+  router.put('/theme', loginRequiredStrictly, adminRequired, csrf, validator.theme, apiV3FormValidator, async(req, res) => {
     const requestParams = {
     const requestParams = {
-      'customize:layout': req.body.layoutType,
       'customize:theme': req.body.themeType,
       'customize:theme': req.body.themeType,
     };
     };
 
 
     try {
     try {
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       await crowi.configManager.updateConfigsInTheSameNamespace('crowi', requestParams);
       const customizedParams = {
       const customizedParams = {
-        layoutType: await crowi.configManager.getConfig('crowi', 'customize:layout'),
         themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
         themeType: await crowi.configManager.getConfig('crowi', 'customize:theme'),
       };
       };
       return res.apiv3({ customizedParams });
       return res.apiv3({ customizedParams });
     }
     }
     catch (err) {
     catch (err) {
-      const msg = 'Error occurred in updating layout and theme';
+      const msg = 'Error occurred in updating theme';
       logger.error('Error', err);
       logger.error('Error', err);
-      return res.apiv3Err(new ErrorV3(msg, 'update-layoutTheme-failed'));
+      return res.apiv3Err(new ErrorV3(msg, 'update-theme-failed'));
     }
     }
   });
   });
 
 

+ 0 - 1
src/server/routes/apiv3/pages.js

@@ -92,7 +92,6 @@ module.exports = (crowi) => {
 
 
     try {
     try {
       const result = await Page.findListWithDescendants(path, req.user, queryOptions);
       const result = await Page.findListWithDescendants(path, req.user, queryOptions);
-
       return res.apiv3(result);
       return res.apiv3(result);
     }
     }
     catch (err) {
     catch (err) {

+ 0 - 49
src/server/views/layout-kibela/base/layout.html

@@ -1,49 +0,0 @@
-{% extends '../../layout/layout.html' %}
-
-{% block html_additional_headers %}
-  {% parent %}
-  {{ cdnScriptTag('highlight-addons') }}
-  {{ cdnScriptTag('drawio-viewer') }}
-{% endblock %}
-
-{% block layout_main %}
-<div class="container-fluid p-0">
-
-  <div id="grw-subnav-switcher-container" class="d-edit-none"></div>
-  <div id="grw-subnav-sticky-trigger" class="sticky-top"></div>
-  <div id="grw-fav-sticky-trigger" class="sticky-top"></div>
-
-  <div class="row body m-0 p-0 d-print-block">
-
-    <div id="main" class="main col-12 kibela-block round-corner {% if page %}{{ css.grant(page) }}{% endif %}{% block main_css_class %}{% endblock %}">
-      {% block content_header_wrapper %}
-        <header class="row mb-5 grw-subnav d-edit-none d-print-block round-corner">
-            <div class="col-12 px-0 mx-0">
-              {% block content_header %}
-                <div id="grw-subnav-container" class="d-edit-none"></div>
-              {% endblock %}
-            </div>
-          </header>
-        </header>
-      {% endblock %}
-
-      <!-- /.grw-subnav -->
-
-      {% block content_main_before %}
-      {% endblock %}
-
-      {% block content_main %}
-      {% endblock content_main %}
-
-      {% block content_main_after%}
-      {% endblock %}
-    </div>
-
-  </div>
-
-</div>
-<!-- /.container-fluid -->
-
-<footer class="footer">
-</footer>
-{% endblock %} {# layout_main #}

+ 0 - 13
src/server/views/layout-kibela/expired_shared_page.html

@@ -1,13 +0,0 @@
-{% extends './shared_page.html' %}
-
-{% block content_header %}
-{% endblock %}
-
-{% block content_page %}
-  <div class="col-md-12">
-    <h2 class="text-muted">
-      <i class="icon-ban" aria-hidden="true"></i>
-      Page is expired
-    </h2>
-  </div>
-{% endblock %}

+ 0 - 19
src/server/views/layout-kibela/forbidden.html

@@ -1,19 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block content_main_before %}
-  {% include '../widget/page_alerts.html' %}
-{% endblock %}
-
-
-{% block content_main %}
-  <div class="row">
-    <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
-      {% include '../widget/forbidden_content.html' %}
-    </div>
-  </div>
-{% endblock %}
-
-{% block body_end %}
-  <div id="crowi-modals">
-  </div>
-{% endblock %}

+ 0 - 19
src/server/views/layout-kibela/not_creatable.html

@@ -1,19 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block content_main_before %}
-  {% include '../widget/page_alerts.html' %}
-{% endblock %}
-
-
-{% block content_main %}
-  <div class="row">
-    <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
-      {% include '../widget/not_creatable_content.html' %}
-    </div>
-  </div>
-{% endblock %}
-
-{% block body_end %}
-  <div id="crowi-modals">
-  </div>
-{% endblock %}

+ 0 - 24
src/server/views/layout-kibela/not_found.html

@@ -1,24 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block content_main_before %}
-  {% include '../widget/page_alerts.html' %}
-{% endblock %}
-
-
-{% block content_main %}
-  <div class="row">
-    <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
-      {% include '../widget/not_found_content.html' %}
-    </div>
-    <div class="col-xl-3 col-lg-4 d-none d-lg-block revision-toc-container"></div>
-  </div>
-{% endblock %}
-
-{% block body_end %}
-  <div id="presentation-layer" class="fullscreen-layer">
-    <div id="presentation-container"></div>
-  </div>
-
-  <div id="crowi-modals">
-  </div>
-{% endblock %}

+ 0 - 13
src/server/views/layout-kibela/not_found_shared_page.html

@@ -1,13 +0,0 @@
-{% extends './shared_page.html' %}
-
-{% block content_header %}
-{% endblock %}
-
-{% block content_page %}
-  <div class="col-md-12">
-    <h2 class="text-muted">
-      <i class="icon-info" aria-hidden="true"></i>
-      Page is not found
-    </h2>
-  </div>
-{% endblock %}

+ 0 - 40
src/server/views/layout-kibela/page.html

@@ -1,40 +0,0 @@
-{% extends 'base/layout.html' %}
-
-{% block content_main_before %}
-{% endblock %}
-
-
-{% block content_main %}
-<div class="row">
-
-  <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
-
-    {% include '../widget/page_content.html' %}
-
-  </div>
-
-  <div class="col-xl-3 col-lg-4 d-none d-lg-block revision-toc-container">
-    <div id="revision-toc" class="revision-toc sps sps--abv" data-sps-offset="80">
-      <div id="revision-toc-content" class="revision-toc-content"></div>
-    </div>
-  </div> {# /.col- #}
-
-</div>
-
-{% endblock %}
-
-
-{% block content_main_after %}
-  {% include 'widget/comments.html' %}
-
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
-{% endblock %}
-
-
-{% block body_end %}
-  <div id="presentation-layer" class="fullscreen-layer">
-    <div id="presentation-container"></div>
-  </div>
-{% endblock %}

+ 0 - 43
src/server/views/layout-kibela/page_list.html

@@ -1,43 +0,0 @@
-{% extends 'base/layout.html' %}
-
- {% block content_main_before%}
- {% endblock %}
-
- {% block content_main %}
-<div class="row page-content">
-
-
-  <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
-
-    {% include '../widget/page_content.html' %}
-
-  </div>
-
-  <div class="col-xl-3 col-lg-4 d-none d-lg-block revision-toc-container">
-    <div id="revision-toc" class="revision-toc sps sps--abv" data-sps-offset="80">
-      <div id="revision-toc-content" class="revision-toc-content"></div>
-    </div>
-  </div>
-
-</div>
-
-  <div class="row page-list bg-white round-corner grw-pt-10px my-5 d-edit-none {% if page.isTopPage() %}mt-5{% endif %}">
-    <div class="col">
-      {% include '../widget/page_list_and_timeline_kibela.html' %}
-    </div>
-  </div>
-{% endblock %}
-
-
-{% block content_main_after %}
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif%}
-{% endblock %}
-
-
-{% block body_end %}
-<div id="presentation-layer" class="fullscreen-layer">
-  <div id="presentation-container"></div>
-</div>
-{% endblock %}

+ 0 - 46
src/server/views/layout-kibela/shared_page.html

@@ -1,46 +0,0 @@
-{% extends 'base/layout.html' %}
-
-
-{% block content_header %}
-  <h1 class="p-3">{{ page.path | preventXss }}</h1>
-{% endblock %}
-
-
-{% block content_main_before %}
-{% endblock %}
-{% block search %}
-{% endblock %}
-{% block head_warn_alert_siteurl_undefined %}
-{% endblock %}
-
-{% block content_main %}
-  <div class="row" 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-created-at="{{ sharelink.createdAt|datetz('Y/m/d H:i:s')}}">
-    {% block content_page %}
-      <div class="col-12 col-xl-9 col-lg-8 bg-white round-corner">
-        <div id="share-link-alert"></div>
-
-        {% include '../widget/page_content.html' %}
-        {# force remove #revision-toc from #content_main of parent #}
-        <script>
-          $('#revision-toc').remove();
-        </script>
-
-      </div>
-
-      {# relocate #revision-toc #}
-      <div class="col-xl-3 col-lg-4 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>
-    {% endblock %}
-
-  </div>
-{% endblock %}
-
-
-{% block body_end %}
-  <div id="presentation-layer" class="fullscreen-layer">
-    <div id="presentation-container"></div>
-  </div>
-{% endblock %}

+ 0 - 55
src/server/views/layout-kibela/user_page.html

@@ -1,55 +0,0 @@
-{% extends 'page.html' %}
-
-{% block main_css_class %}
-  {% parent %}
-  user-page
-{% endblock %}
-
-
-{% block content_main %}
-  <div class="row pt-15">
-
-    <div class="col bg-white round-corner">
-
-      {#
-        # ensure to insert 'user_page_content' widget to here
-        #
-        #   Because this block has content like 'Bookmarks' or 'Recent Created' whose height changes dynamically,
-        #   setting of 'revision-toc' (affix) is hindered.
-        #}
-      <div class="mb-5 user-page-content-container d-edit-none">
-        {% include '../widget/user_page_content.html' %}
-      </div>
-
-      {% block content_main_before %}
-        {% parent %}
-      {% endblock %}
-
-      {% include '../widget/page_content.html' %}
-
-    </div>
-
-    <div class="col-xl-3 col-lg-4 d-none d-lg-block revision-toc-container">
-      <div id="revision-toc" class="revision-toc sps sps--abv" data-sps-offset="75">
-        <div id="revision-toc-content" class="revision-toc-content"></div>
-      </div>
-    </div>
-
-  </div>
-
-  <div class="row page-list mt-5 d-edit-none">
-    <div class="col-12">
-      {% include '../widget/page_list_and_timeline_kibela.html' %}
-    </div>
-  </div>
-
-{% endblock %}
-
-
-{% block content_main_after %}
-  {% include 'widget/comments.html' %}
-
-  {% if page %}
-    {% include '../widget/page_attachments.html' %}
-  {% endif %}
-{% endblock %}

+ 0 - 15
src/server/views/layout-kibela/widget/comments.html

@@ -1,15 +0,0 @@
-<div class="page-comments-row row d-edit-none">
-
-    <div class="page-comments col-12">
-
-      <h4 class="my-2"><i class="icon-fw icon-bubbles"></i> Comments</h4>
-
-      <div class="page-comments-list" id="page-comments-list"></div>
-
-      {% if page and not page.isDeleted() %}
-      <div id="page-comment-write"></div>
-      {% endif %}
-
-    </div>
-</div>
-

+ 5 - 10
src/server/views/layout/layout.html

@@ -43,14 +43,10 @@
 
 
   <!-- styles -->
   <!-- styles -->
   {% include '../widget/headers/styles-for-app.html' %}
   {% include '../widget/headers/styles-for-app.html' %}
-  {% if 'kibela' === getConfig('crowi', 'customize:layout') %}
-    {% include '../widget/headers/styles-theme-kibela.html' %}
-  {% else %}
-    {% block theme_css_block %}
-      {% set themeName = getConfig('crowi', 'customize:theme') %}
-      {% include '../widget/headers/styles-theme.html' with {themeName: themeName} %}
-    {% endblock %}
-  {% endif %}
+  {% block theme_css_block %}
+    {% set themeName = getConfig('crowi', 'customize:theme') %}
+    {% include '../widget/headers/styles-theme.html' with {themeName: themeName} %}
+  {% endblock %}
 
 
   {{ cdnStyleTagsByGroup('basis') }}
   {{ cdnStyleTagsByGroup('basis') }}
   {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
   {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
@@ -65,8 +61,7 @@
 
 
 {% block html_body %}
 {% block html_body %}
 <body
 <body
-  class="{% block html_base_css %}{% endblock %}
-      {% if !getConfig('crowi', 'customize:layout') || 'crowi' === getConfig('crowi', 'customize:layout') %}crowi{% elseif !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout') %}kibela{% else %}growi{% endif %}"
+  class="{% block html_base_css %}{% endblock %} growi"
   data-is-admin="{{ user.admin }}"
   data-is-admin="{{ user.admin }}"
   data-plugin-enabled="{{ getConfig('crowi', 'plugin:isEnabledPlugins') }}"
   data-plugin-enabled="{{ getConfig('crowi', 'plugin:isEnabledPlugins') }}"
   {% block html_base_attr %}{% endblock %}
   {% block html_base_attr %}{% endblock %}

+ 0 - 5
src/server/views/widget/headers/styles-theme-kibela.html

@@ -1,5 +0,0 @@
-{% if env === 'development' %}
-  <script src="{{ webpack_asset('styles/theme-kibela.js') }}"></script>
-{% else %}
-  <link rel="stylesheet" href="{{ webpack_asset('styles/theme-kibela.css') }}">
-{% endif %}

+ 1 - 5
src/server/views/widget/page_content.html

@@ -41,11 +41,7 @@
 
 
   {% include 'page_alerts.html' %}
   {% include 'page_alerts.html' %}
 
 
-  {% if !getConfig('crowi', 'customize:layout') || 'kibela' === getConfig('crowi', 'customize:layout') %}
-    {% include 'page_tabs_kibela.html' %}
-  {% else %}
-    {% include 'page_tabs.html' %}
-  {% endif %}
+  {% include 'page_tabs.html' %}
 
 
   <div class="tab-content">
   <div class="tab-content">
 
 

+ 0 - 38
src/server/views/widget/page_list_and_timeline_kibela.html

@@ -1,38 +0,0 @@
-<div class="page-list-container">
-  <ul class="nav nav-tabs" role="tablist">
-      <li class="nav-item">
-        <a class="nav-link active" href="#view-list" role="tab" data-toggle="tab">{{ t('List View') }}</a>
-      </li>
-      {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
-      <li class="nav-item">
-        <a class="nav-link" href="#view-timeline" role="tab" data-toggle="tab">{{ t('Timeline View') }}</a>
-      </li>
-      {% endif %}
-  </ul>
-
-  <div class="tab-content">
-    {# list view #}
-    <div class="pt-2 active tab-pane page-list-container fade show" id="view-list">
-      {% if pages.length == 0 %}
-        <div class="mt-2">
-          {% if isTrashPage() %}
-          No deleted pages.
-          {% else %}
-          There are no pages under <strong>{{ path | preventXss }}</strong>.
-          {% endif %}
-        </div>
-      {% else %}
-        {% include 'page_list.html' with { pages: pages, pager: pager, viewConfig: viewConfig } %}
-      {% endif %}
-    </div>
-
-    {# timeline view #}
-    {% if getConfig('crowi', 'customize:isEnabledTimeline') %}
-      <div class="tab-pane mt-5" id="view-timeline">
-        <script type="text/template" id="page-timeline-data">{{ JSON.stringify(pagesDataForTimeline(pages)) | preventXss }}</script>
-        {# render React Component PageTimeline #}
-        <div id="page-timeline"></div>
-      </div>
-    {% endif %}
-  </div>
-</div>

+ 0 - 19
src/server/views/widget/page_tabs.html

@@ -47,25 +47,6 @@
   {# to place right side #}
   {# to place right side #}
   <div class="mr-auto"></div>
   <div class="mr-auto"></div>
 
 
-<<<<<<< HEAD
-=======
-  <!-- presentation -->
-  {% if not page.isTopPage() %}
-    <li class="nav-item d-edit-none">
-      <a href="?presentation=1&revisionId={{revision.id}}" class="nav-link toggle-presentation">
-        <i class="icon-film icon-fw"></i><span class="d-none d-sm-inline">{{ t('Presentation Mode') }}</span>
-      </a>
-    </li>
-  {% endif %}
-
->>>>>>> master
-  <!-- revision-history -->
-  <li class="nav-item d-edit-none">
-    <a class="nav-link" href="#revision-history" role="tab" data-toggle="tab">
-      <i class="icon-layers icon-fw"></i><span class="d-none d-md-inline">{{ t('History') }}</span>
-    </a>
-  </li>
-
   <!-- icon-options-vertical -->
   <!-- icon-options-vertical -->
   {% if !isTrashPage() %}
   {% if !isTrashPage() %}
     <li id="page-management" class="nav-item dropdown d-edit-none"></li>
     <li id="page-management" class="nav-item dropdown d-edit-none"></li>

+ 0 - 69
src/server/views/widget/page_tabs_kibela.html

@@ -1,69 +0,0 @@
-{% if page %}
-<ul class="nav nav-tabs d-print-none">
-
-  {#
-    Left Tabs
-  #}
-  <li class="nav-item active">
-    <a class="nav-link active" href="#revision-body" data-toggle="tab">
-      <i class="icon-control-play"></i> View
-    </a>
-  </li>
-
-  {% if !isTrashPage() %}
-    <li class="nav-item nav-tab-edit">
-      <a
-        {% if user %} href="#edit" data-toggle="tab" class="nav-link edit-button" {% endif %}
-        {% if not user %}
-          class="nav-link edit-button edit-button-disabled"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="icon-note"></i> {{ t('Edit') }}
-      </a>
-    </li>
-    {% if isHackmdSetup() %}
-    <li class="nav-item nav-tab-hackmd">
-      <a
-        {% if user %} href="#hackmd" data-toggle="tab" class="nav-link edit-button" {% endif %}
-        {% if not user %}
-          class="nav-link edit-button edit-button-disabled"
-          data-toggle="tooltip" data-placement="top" data-container="body" title="{{ t('Not available for guest') }}"
-        {% endif %}
-      >
-        <i class="fa fa-file-text-o"></i> {{ t('HackMD') }}
-      </a>
-    </li>
-    {% endif %}
-
-    <div id="page-editor-path-nav" class="d-none d-edit-sm-block ml-2"></div>
-
-  {% endif %}
-
-  {#
-    Right Tabs
-  #}
-  {# to place right side #}
-  <div class="mr-auto"></div>
-
-  {% if not page.isTopPage() %}
-  <li class="nav-item">
-    <a href="?presentation=1" class="nav-link toggle-presentation">
-      <i class="icon-film"></i><span class="d-none d-sm-inline"> {{ t('Presentation Mode') }}</span>
-    </a>
-  </li>
-  {% endif %}
-
-  <li class="nav-item">
-    <a href="#revision-history" class="nav-link" data-toggle="tab">
-      <i class="icon-layers"></i><span class="d-none d-sm-inline"> {{ t('History') }}</span>
-    </a>
-  </li>
-
-  {% if !isTrashPage() %}
-    <li id="page-management" class="nav-item dropdown d-edit-none"></li>
-  {% endif %}
-
-</ul>
-
-{% endif %}

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

@@ -32,7 +32,7 @@
 
 
     <div class="tab-pane user-bookmark-list page-list active" id="user-bookmark-list">
     <div class="tab-pane user-bookmark-list page-list active" id="user-bookmark-list">
       {% if bookmarkList.length == 0 %}
       {% if bookmarkList.length == 0 %}
-        No bookmarks yet.
+        {{t('No bookmarks yet')}}.
       {% else %}
       {% else %}
         <div class="page-list-container">
         <div class="page-list-container">
           {% include 'page_list.html' with { pages: bookmarkList, pagePropertyName: 'page' } %}
           {% include 'page_list.html' with { pages: bookmarkList, pagePropertyName: 'page' } %}