Jelajahi Sumber

Merge branch 'dev/4.0.x' into imprv/christmas-island-color-migration

kaoritokashiki 5 tahun lalu
induk
melakukan
6b2344a1b9
100 mengubah file dengan 2470 tambahan dan 1352 penghapusan
  1. 1 1
      config/webpack.common.js
  2. 1 0
      package.json
  3. 4 1
      resource/locales/en-US/admin/admin.json
  4. 7 6
      resource/locales/en-US/translation.json
  5. 4 1
      resource/locales/ja/admin/admin.json
  6. 9 8
      resource/locales/ja/translation.json
  7. 6 3
      src/client/js/app.jsx
  8. 2 3
      src/client/js/bootstrap.jsx
  9. 15 19
      src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx
  10. 16 6
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  11. 11 10
      src/client/js/components/Admin/Customize/ThemeColorBox.jsx
  12. 1 1
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx
  13. 7 11
      src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx
  14. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx
  15. 2 2
      src/client/js/components/Admin/Notification/GlobalNotificationList.jsx
  16. 2 2
      src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx
  17. 13 17
      src/client/js/components/Admin/Security/SecuritySetting.jsx
  18. 1 1
      src/client/js/components/Admin/UserManagement.jsx
  19. 3 5
      src/client/js/components/Admin/Users/ExternalAccountTable.jsx
  20. 1 1
      src/client/js/components/Admin/Users/RemoveAdminButton.jsx
  21. 1 1
      src/client/js/components/Admin/Users/StatusActivateButton.jsx
  22. 1 1
      src/client/js/components/Admin/Users/StatusSuspendedButton.jsx
  23. 1 1
      src/client/js/components/Admin/Users/UserRemoveButton.jsx
  24. 90 0
      src/client/js/components/CreateTemplateModal.jsx
  25. 73 0
      src/client/js/components/EmptyTrashModal.jsx
  26. 3 3
      src/client/js/components/InstallerForm.jsx
  27. 316 0
      src/client/js/components/LoginForm.jsx
  28. 3 5
      src/client/js/components/Me/BasicInfoSettings.jsx
  29. 1 1
      src/client/js/components/Me/ExternalAccountRow.jsx
  30. 1 2
      src/client/js/components/Me/PersonalSettings.jsx
  31. 3 13
      src/client/js/components/Navbar/GrowiSubNavigation.jsx
  32. 2 10
      src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx
  33. 0 5
      src/client/js/components/Navbar/PersonalDropdown.jsx
  34. 42 15
      src/client/js/components/Navbar/SearchTop.jsx
  35. 150 0
      src/client/js/components/Page/PageManagement.jsx
  36. 138 0
      src/client/js/components/Page/TrashPageAlert.jsx
  37. 88 58
      src/client/js/components/PageCreateModal.jsx
  38. 161 0
      src/client/js/components/PageDeleteModal.jsx
  39. 130 0
      src/client/js/components/PageDuplicateModal.jsx
  40. 83 0
      src/client/js/components/PageManagement/ApiErrorMessage.jsx
  41. 38 40
      src/client/js/components/PagePathAutoComplete.jsx
  42. 172 0
      src/client/js/components/PageRenameModal.jsx
  43. 104 0
      src/client/js/components/PutbackPageModal.jsx
  44. 1 1
      src/client/js/components/RecentCreated/RecentCreated.jsx
  45. 25 37
      src/client/js/components/Sidebar.jsx
  46. 0 24
      src/client/js/installer.jsx
  47. 0 256
      src/client/js/legacy/crowi.js
  48. 81 0
      src/client/js/nologin.jsx
  49. 32 1
      src/client/js/services/AppContainer.js
  50. 23 0
      src/client/js/services/NoLoginContainer.js
  51. 66 2
      src/client/js/services/PageContainer.js
  52. 12 0
      src/client/js/util/apiv1ErrorHandler.js
  53. 0 78
      src/client/styles/agile-admin/inverse/variables.scss
  54. 1 5
      src/client/styles/scss/_admin.scss
  55. 14 60
      src/client/styles/scss/_create-page.scss
  56. 2 1
      src/client/styles/scss/_editor-attachment.scss
  57. 2 1
      src/client/styles/scss/_hljs.scss
  58. 2 2
      src/client/styles/scss/_layout.scss
  59. 57 189
      src/client/styles/scss/_login.scss
  60. 0 3
      src/client/styles/scss/_me.scss
  61. 25 24
      src/client/styles/scss/_override-bootstrap-variables.scss
  62. 2 9
      src/client/styles/scss/_override-bootstrap.scss
  63. 21 11
      src/client/styles/scss/_search.scss
  64. 1 1
      src/client/styles/scss/_sidebar.scss
  65. 0 42
      src/client/styles/scss/_subnav.scss
  66. 0 4
      src/client/styles/scss/_tag.scss
  67. 0 35
      src/client/styles/scss/_user.scss
  68. 13 0
      src/client/styles/scss/_vendor-presentation.scss
  69. 0 9
      src/client/styles/scss/_wiki.scss
  70. 8 1
      src/client/styles/scss/atoms/_buttons.scss
  71. 11 0
      src/client/styles/scss/atoms/_code.scss
  72. 4 0
      src/client/styles/scss/atoms/_pre.scss
  73. 2 1
      src/client/styles/scss/style-app.scss
  74. 4 7
      src/client/styles/scss/style-presentation.scss
  75. 4 17
      src/client/styles/scss/theme/_apply-colors-dark.scss
  76. 5 0
      src/client/styles/scss/theme/_apply-colors-kibela.scss
  77. 4 14
      src/client/styles/scss/theme/_apply-colors-light.scss
  78. 36 15
      src/client/styles/scss/theme/_apply-colors.scss
  79. 0 2
      src/client/styles/scss/theme/_reboot-bootstrap-colors.scss
  80. 1 0
      src/client/styles/scss/theme/_reboot-bootstrap-nav.scss
  81. 21 11
      src/client/styles/scss/theme/antarctic.scss
  82. 3 2
      src/client/styles/scss/theme/christmas.scss
  83. 28 8
      src/client/styles/scss/theme/default.scss
  84. 3 2
      src/client/styles/scss/theme/future.scss
  85. 3 2
      src/client/styles/scss/theme/halloween.scss
  86. 3 0
      src/client/styles/scss/theme/island.scss
  87. 16 67
      src/client/styles/scss/theme/kibela.scss
  88. 54 43
      src/client/styles/scss/theme/mono-blue.scss
  89. 41 40
      src/client/styles/scss/theme/nature.scss
  90. 21 12
      src/client/styles/scss/theme/spring.scss
  91. 50 26
      src/client/styles/scss/theme/wood.scss
  92. 4 2
      src/lib/models/devided-page-path.js
  93. 6 4
      src/lib/models/linked-page-path.js
  94. 12 11
      src/server/routes/index.js
  95. 9 8
      src/server/routes/login-passport.js
  96. 1 1
      src/server/routes/page.js
  97. 22 4
      src/server/service/customize.js
  98. 1 1
      src/server/views/admin/app.html
  99. 4 1
      src/server/views/admin/customize.html
  100. 1 1
      src/server/views/admin/export.html

+ 1 - 1
config/webpack.common.js

@@ -22,7 +22,7 @@ module.exports = (options) => {
     entry: Object.assign({
       'js/app':                       './src/client/js/app',
       'js/admin':                     './src/client/js/admin',
-      'js/installer':                 './src/client/js/installer',
+      'js/nologin':                   './src/client/js/nologin',
       'js/legacy':                    './src/client/js/legacy/crowi',
       'js/legacy-presentation':       './src/client/js/legacy/crowi-presentation',
       'js/plugin':                    './src/client/js/plugin',

+ 1 - 0
package.json

@@ -134,6 +134,7 @@
     "passport-local": "^1.0.0",
     "passport-saml": "^1.0.0",
     "passport-twitter": "^1.0.4",
+    "react-card-flip": "^1.0.10",
     "react-image-crop": "^8.3.0",
     "rimraf": "^3.0.0",
     "slack-node": "^0.1.8",

+ 4 - 1
resource/locales/en-US/admin/admin.json

@@ -125,7 +125,10 @@
     "code_highlight": "Code highlight",
     "nocdn_desc": "This function is disabled when the environment variable <code>NO_CDN=true</code>.<br>Github style has been forcibly applied.",
     "custom_title": "Custom title",
-    "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag.<br><code>&#123;&#123;sitename&#125;&#125;</code> will be automatically replaced with the app name, and <code>&#123;&#123;page&#125;&#125;</code> will be replaced with the page name/path.",
+    "custom_title_detail": "You can customize <code>&lt;title&gt;</code> tag. Following placeholders will be automatically replaced:",
+    "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - The site name of this wiki.",
+    "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - The page name of the current page.",
+    "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - The page path of the current page.",
     "custom_header": "Custom HTML header",
     "custom_header_detail": "You can customize HTML header that applies all pages. Your custom script will be inserted in <code>&lt;header&gt;</code> but above other <code>&lt;script&gt;</code> tags.<br>Relaod page to see changes.",
     "custom_css": "Custom CSS",

+ 7 - 6
resource/locales/en-US/translation.json

@@ -270,9 +270,9 @@
       "Redirect": "Redirect"
     },
     "help": {
-      "redirect": "Redirect to new page if someone accesses <code>%s</code>",
+      "redirect": "Redirect to new page if someone accesses under this path",
       "metadata": "Remains last update user and updated date as is",
-      "recursive": "Move/Rename children of under <code>%s</code> recursively"
+      "recursive": "Move/Rename children of under this path recursively"
     }
   },
   "Put Back": "Put back",
@@ -283,11 +283,12 @@
     "delete_recursively": "Delete child pages recursively.",
     "delete_completely": "Delete completely",
     "delete_completely_restriction": "You don't have the authority to delete pages completely.",
-    "recursively": "Delete children of <code>%s</code> recursively.",
+    "recursively": "Delete pages under this path recursively.",
     "completely": "Delete completely instead of putting it into trash."
   },
   "modal_empty":{
-    "empty_the_trash": "Empty The Trash"
+    "empty_the_trash": "Empty The Trash",
+    "notice": "The pages deleted completely are unrecoverable."
   },
   "modal_duplicate": {
     "label": {
@@ -302,7 +303,7 @@
       "recursively": "Put back recursively"
     },
     "help": {
-      "recursively": "Put back children of under <code>%s</code> recursively"
+      "recursively": "Put back page under this path recursively"
     }
   },
   "modal_shortcuts": {
@@ -339,7 +340,7 @@
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit template page",
-      "Create template under": "Create template page under:"
+      "Create template under": "Create template page under this page"
     },
     "option_label": {
       "create/edit": "Create/Edit template page..",

+ 4 - 1
resource/locales/ja/admin/admin.json

@@ -125,7 +125,10 @@
     "code_highlight": "コードハイライト",
     "nocdn_desc": "この機能は、環境変数 <code>NO_CDN=true</code> の時は無効化されます。<br>GitHub スタイルが適用されています。",
     "custom_title": "カスタム Title",
-    "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。<br><code>&#123;&#123;sitename&#125;&#125;</code>がサイト名、<code>&#123;&#123;page&#125;&#125;</code>がページ名またはページパスに置換されます。",
+    "custom_title_detail": "<code>&lt;title&gt;</code>タグのコンテンツをカスタマイズできます。以下のプレースホルダーは自動的に置換されます:",
+    "custom_title_detail_placeholder1": "<code>&#123;&#123;sitename&#125;&#125;</code> - この Wiki のサイト名",
+    "custom_title_detail_placeholder2": "<code>&#123;&#123;pagename&#125;&#125;</code> - 現在表示中のページ名",
+    "custom_title_detail_placeholder3": "<code>&#123;&#123;pagepath&#125;&#125;</code> - 現在表示中のページパス",
     "custom_header": "カスタム HTML Header",
     "custom_header_detail": "システム全体に適用される HTML を記述できます。<code>&lt;header&gt;</code> タグ内の他の <code>&lt;script&gt;</code> タグ読み込み前に展開されます。<br>変更の反映はページの更新が必要です。",
     "custom_css": "カスタム CSS",

+ 9 - 8
resource/locales/ja/translation.json

@@ -268,9 +268,9 @@
       "Redirect": "リダイレクトする"
     },
     "help": {
-      "redirect": "<code class='text-break'>%s</code> にアクセスされた際に自動的に新しいページにジャンプします",
+      "redirect": "アクセスされた際に自動的に新しいページにジャンプします",
       "metadata": "最終更新ユーザー、最終更新日を更新せず維持します",
-      "recursive": "<code class='text-break'>%s</code> 配下のページも移動/名前変更します"
+      "recursive": "配下のページも移動/名前変更します"
     }
   },
   "Put Back": "元に戻す",
@@ -281,11 +281,12 @@
     "delete_recursively": "全ての子ページも削除",
     "delete_completely": "完全削除",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
-    "recursively": "<code>%s</code> 配下のページも削除します",
+    "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   "modal_empty":{
-    "empty_the_trash": "ゴミ箱を空にする"
+    "empty_the_trash": "ゴミ箱を空にする",
+    "notice": "完全削除したページは元に戻すことができません"
   },
   "modal_duplicate": {
     "label": {
@@ -300,7 +301,7 @@
       "recursively": "全ての子ページも元に戻す"
     },
     "help": {
-      "recursively": "<code>%s</code> 配下のページも元に戻します"
+      "recursively": "配下のページも元に戻します"
     }
   },
   "modal_shortcuts": {
@@ -314,7 +315,7 @@
       "konami_code_url": "https://ja.wikipedia.org/wiki/コナミコマンド"
     },
     "editor": {
-      "titile": "エディターショートカット",
+      "title": "エディターショートカット",
       "Indent": "インデント",
       "Outdent": "左インデント",
       "Save Page": "保存",
@@ -332,12 +333,12 @@
     "activate_user_success": "{{username}}を有効化しました",
     "deactivate_user_success": "{{username}}を無効化しました",
     "remove_user_success": "{{username}}を削除しました",
-    "remove_external_user_success": "{{accountId}}を削除しました "
+    "remove_external_user_success": "{{accountId}}を削除しました"
   },
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "以下のパスにテンプレートページを作成"
+      "Create template under": "配下にテンプレートページを作成"
     },
     "option_label": {
       "select": "テンプレートタイプを選択してください",

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

@@ -21,6 +21,8 @@ import PageHistory from './components/PageHistory';
 import PageComments from './components/PageComments';
 import PageTimeline from './components/PageTimeline';
 import CommentEditorLazyRenderer from './components/PageComment/CommentEditorLazyRenderer';
+import PageManagement from './components/Page/PageManagement';
+import TrashPageAlert from './components/Page/TrashPageAlert';
 import PageAttachment from './components/PageAttachment';
 import PageStatusAlert from './components/PageStatusAlert';
 import PagePathAutoComplete from './components/PagePathAutoComplete';
@@ -68,14 +70,14 @@ Object.assign(componentMappings, {
   // 'revision-history': <PageHistory pageId={pageId} />,
   'tags-page': <TagsList crowi={appContainer} />,
 
-  'create-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} addTrailingSlash />,
-
   'page-editor': <PageEditor />,
   'page-editor-path-nav': <PagePathNavForEditor />,
   'page-editor-options-selector': <OptionsSelector crowi={appContainer} />,
   'page-status-alert': <PageStatusAlert />,
   'save-page-controls': <SavePageControls />,
 
+  'trash-page-alert': <TrashPageAlert />,
+
   'page-timeline': <PageTimeline />,
 
   'personal-setting': <PersonalSettings crowi={personalContainer} />,
@@ -88,11 +90,12 @@ if (pageContainer.state.pageId != null) {
     'page-comments-list': <PageComments />,
     'page-attachment': <PageAttachment />,
     'page-comment-write': <CommentEditorLazyRenderer />,
+    'page-management': <PageManagement />,
+
     'revision-toc': <TableOfContents />,
     'seen-user-list': <UserPictureList userIds={pageContainer.state.seenUserIds} />,
     'liker-list': <UserPictureList userIds={pageContainer.state.likerUserIds} />,
     'rename-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
-    'duplicate-page-name-input': <PagePathAutoComplete crowi={appContainer} initializedPath={pageContainer.state.path} />,
 
     'user-created-list': <RecentCreated />,
     'user-draft-list': <MyDraftList />,

+ 2 - 3
src/client/js/bootstrap.jsx

@@ -3,7 +3,7 @@ import React from 'react';
 import loggerFactory from '@alias/logger';
 import Xss from '@commons/service/xss';
 
-import HeaderSearchBox from './components/HeaderSearchBox';
+import SearchTop from './components/Navbar/SearchTop';
 import NavbarToggler from './components/Navbar/NavbarToggler';
 import PersonalDropdown from './components/Navbar/PersonalDropdown';
 import Sidebar from './components/Sidebar';
@@ -42,8 +42,7 @@ appContainer.injectToWindow();
 const componentMappings = {
   'grw-navbar-toggler': <NavbarToggler />,
 
-  'search-top': <HeaderSearchBox />,
-  'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
+  'grw-search-top': <SearchTop />,
   'personal-dropdown': <PersonalDropdown />,
 
   'create-page-button': <PageCreateButton />,

+ 15 - 19
src/client/js/components/Admin/Customize/CustomizeThemeOptions.jsx

@@ -15,29 +15,31 @@ class CustomizeThemeOptions extends React.Component {
     const { t, adminCustomizeContainer } = this.props;
     const { currentLayout, currentTheme } = adminCustomizeContainer.state;
 
+    /* eslint-disable no-multi-spaces */
     const lightNDarkTheme = [{
-      name: 'default', bg: '#ffffff', topbar: '#334455', theme: '#112744',
+      name: 'default',    bg: '#ffffff', topbar: '#2a2929', sidebar: '#122c55', theme: '#209fd8',
     }, {
-      name: 'mono-blue', bg: '#F7FBFD', topbar: '#00587A', theme: '#00587A',
+      name: 'mono-blue',  bg: '#F7FBFD', topbar: '#2a2929', sidebar: '#00587A', theme: '#00587A',
     }];
 
     const uniqueTheme = [{
-      name: 'nature', bg: '#f9fff3', topbar: '#118050', theme: '#460039',
+      name: 'nature',     bg: '#f9fff3', topbar: '#2a2929', sidebar: '#118050', theme: '#460039',
     }, {
-      name: 'wood', bg: '#fffefb', topbar: '#aaa45f', theme: '#dddebf',
+      name: 'wood',       bg: '#fffefb', topbar: '#2a2929', sidebar: '#aaa45f', theme: '#dddebf',
     }, {
-      name: 'island', bg: '#8ecac0', topbar: '#0c2a44', theme: '#cef2ef',
+      name: 'island',     bg: '#8ecac0', topbar: '#2a2929', sidebar: '#0c2a44', theme: '#cef2ef',
     }, {
-      name: 'christmas', bg: '#fffefb', topbar: '#b3000c', theme: '#017e20',
+      name: 'christmas',  bg: '#fffefb', topbar: '#2a2929', sidebar: '#b3000c', theme: '#017e20',
     }, {
-      name: 'antarctic', bg: '#ffffff', topbar: '#000080', theme: '#99cccc',
+      name: 'antarctic',  bg: '#ffffff', topbar: '#2a2929', sidebar: '#000080', theme: '#99cccc',
     }, {
-      name: 'spring', bg: '#fff5ee', topbar: '#ff69b4', theme: '#ffb6c1',
+      name: 'spring',     bg: '#fff5ee', topbar: '#2a2929', sidebar: '#ff69b4', theme: '#ffb6c1',
     }, {
-      name: 'future', bg: '#16282D', topbar: '#011414', theme: '#04B4AE',
+      name: 'future',     bg: '#16282D', topbar: '#2a2929', sidebar: '#011414', theme: '#04B4AE',
     }, {
-      name: 'halloween', bg: '#030003', topbar: '#cc5d1f', theme: '#e9af2b',
+      name: 'halloween',  bg: '#030003', topbar: '#2a2929', sidebar: '#cc5d1f', theme: '#e9af2b',
     }];
+    /* eslint-enable no-multi-spaces */
 
     return (
       <div id="themeOptions" className={`${currentLayout === 'kibela' && 'disabled'}`}>
@@ -51,17 +53,14 @@ class CustomizeThemeOptions extends React.Component {
                   key={theme.name}
                   isSelected={currentTheme === theme.name}
                   onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                  name={theme.name}
-                  bg={theme.bg}
-                  topbar={theme.topbar}
-                  theme={theme.theme}
+                  {...theme}
                 />
               );
             })}
           </div>
         </div>
         {/* Unique Theme */}
-        <div>
+        <div className="mt-3">
           <h3>{t('admin:customize_setting.theme_desc.unique')}</h3>
           <div className="d-flex flex-wrap">
             {uniqueTheme.map((theme) => {
@@ -70,10 +69,7 @@ class CustomizeThemeOptions extends React.Component {
                   key={theme.name}
                   isSelected={currentTheme === theme.name}
                   onSelected={() => adminCustomizeContainer.switchThemeType(theme.name)}
-                  name={theme.name}
-                  bg={theme.bg}
-                  topbar={theme.topbar}
-                  theme={theme.theme}
+                  {...theme}
                 />
               );
             })}

+ 16 - 6
src/client/js/components/Admin/Customize/CustomizeTitle.jsx

@@ -44,19 +44,29 @@ class CustomizeTitle extends React.Component {
           <div className="col-12">
             <Card className="card well">
               <CardBody className="px-0 py-2">
-                <span
-                  // eslint-disable-next-line react/no-danger
-                  dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }}
-                />
+                {/* eslint-disable react/no-danger */}
+                <p dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail') }} />
+                <ul>
+                  <li>
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder1') }} />
+                  </li>
+                  <li>
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder2') }} />
+                  </li>
+                  <li>
+                    <span dangerouslySetInnerHTML={{ __html: t('admin:customize_setting.custom_title_detail_placeholder3') }} />
+                  </li>
+                </ul>
+                {/* eslint-enable react/no-danger */}
               </CardBody>
             </Card>
           </div>
 
           {/* TODO i18n */}
           <div className="form-text text-muted col-12">
-            Default Value: <code>&#123;&#123;page&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
+            Default Value: <code>&#123;&#123;pagename&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
             <br />
-            Default Output: <code className="xml">&lt;title&gt;/Somewhere/Page - {'GROWI'}&lt;&#047;title&gt;</code>
+            Default Output Example: <code className="xml">&lt;title&gt;Page name - My GROWI&lt;&#047;title&gt;</code>
           </div>
           <div className="form-group col-12">
             <input

+ 11 - 10
src/client/js/components/Admin/Customize/ThemeColorBox.jsx

@@ -5,23 +5,23 @@ import PropTypes from 'prop-types';
 class ThemeColorBox extends React.PureComponent {
 
   render() {
-    const { name } = this.props;
+    const {
+      isSelected, onSelected, name, bg, topbar, sidebar, theme,
+    } = this.props;
 
     return (
       <div
         id={`theme-option-${name}`}
-        className={`theme-option-container d-flex flex-column align-items-center ${this.props.isSelected && 'active'}`}
-        onClick={this.props.onSelected}
+        className={`theme-option-container d-flex flex-column align-items-center ${isSelected && 'active'}`}
+        onClick={onSelected}
       >
-        <a
-          className={`m-0 ${name} theme-button`}
-          id={name}
-        >
+        <a id={name} type="button" className={`m-0 ${name} theme-button`}>
           <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64">
             <g>
-              <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={this.props.bg}></path>
-              <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={this.props.topbar}></path>
-              <path d="M 44 15 L65 15 L65 65 L44 65 L44 15 Z" fill={this.props.theme}></path>
+              <path d="M -1 -1 L65 -1 L65 65 L-1 65 L-1 -1 Z" fill={bg}></path>
+              <path d="M -1 -1 L65 -1 L65 15 L-1 15 L-1 -1 Z" fill={topbar}></path>
+              <path d="M -1 15 L15 15 L15 65 L-1 65 L-1 15 Z" fill={sidebar}></path>
+              <path d="M 65 45 L65 65 L45 65 L65 45 Z" fill={theme}></path>
             </g>
           </svg>
         </a>
@@ -39,6 +39,7 @@ ThemeColorBox.propTypes = {
   name: PropTypes.string.isRequired,
   bg: PropTypes.string.isRequired,
   topbar: PropTypes.string.isRequired,
+  sidebar: PropTypes.string.isRequired,
   theme: PropTypes.string.isRequired,
 };
 

+ 1 - 1
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTable.jsx

@@ -14,7 +14,7 @@ class ArchiveFilesTable extends React.Component {
     const { t } = this.props;
 
     return (
-      <div className="table-responsive text-nowrap">
+      <div className="table-responsive">
         <table className="table table-bordered">
           <thead>
             <tr>

+ 7 - 11
src/client/js/components/Admin/ExportArchiveData/ArchiveFilesTableMenu.jsx

@@ -12,22 +12,18 @@ class ArchiveFilesTableMenu extends React.Component {
     const { t } = this.props;
 
     return (
-      <div className="btn-group admin-user-menu">
+      <div className="btn-group admin-user-menu dropdown">
         <button type="button" className="btn btn-sm btn-outline-secondary dropdown-toggle" data-toggle="dropdown">
           <i className="icon-settings"></i> <span className="caret"></span>
         </button>
         <ul className="dropdown-menu" role="menu">
           <li className="dropdown-header">{t('admin:export_management.export_menu')}</li>
-          <li>
-            <a type="button" href={`/admin/export/${this.props.fileName}`}>
-              <i className="icon-cloud-download" /> {t('admin:export_management.download')}
-            </a>
-          </li>
-          <li>
-            <a type="button" role="button" onClick={() => this.props.onZipFileStatRemove(this.props.fileName)}>
-              <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
-            </a>
-          </li>
+          <a type="button" className="dropdown-item" href={`/admin/export/${this.props.fileName}`}>
+            <i className="icon-cloud-download" /> {t('admin:export_management.download')}
+          </a>
+          <a type="button" className="dropdown-item" role="button" onClick={() => this.props.onZipFileStatRemove(this.props.fileName)}>
+            <span className="text-danger"><i className="icon-trash" /> {t('admin:export_management.delete')}</span>
+          </a>
         </ul>
       </div>
     );

+ 1 - 1
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionItem.jsx

@@ -131,7 +131,7 @@ export default class ImportCollectionItem extends React.Component {
             { modes.map((mode) => {
               return (
                 <li key={`buttonMode_${mode}`}>
-                  <a type="button" role="button" onClick={() => this.modeSelectedHandler(mode)}>
+                  <a type="button" className="dropdown-item" role="button" onClick={() => this.modeSelectedHandler(mode)}>
                     {this.renderModeLabel(mode, true)}
                   </a>
                 </li>

+ 2 - 2
src/client/js/components/Admin/Notification/GlobalNotificationList.jsx

@@ -103,7 +103,7 @@ class GlobalNotificationList extends React.Component {
                   </li>
                 )}
                   {notification.triggerEvents.includes('pageMove') && (
-                  <li className="list-inline-item badge badge-pill badge-warning" data-toggle="tooltip" data-placement="top" title="Page Move">
+                  <li className="list-inline-item badge badge-pill badge-pink" data-toggle="tooltip" data-placement="top" title="Page Move">
                     <i className="icon-action-redo"></i> MOVE
                   </li>
                 )}
@@ -118,7 +118,7 @@ class GlobalNotificationList extends React.Component {
                   </li>
                 )}
                   {notification.triggerEvents.includes('comment') && (
-                  <li className="list-inline-item badge badge-pill badge-light" data-toggle="tooltip" data-placement="top" title="New Comment">
+                  <li className="list-inline-item badge badge-pill badge-secondary" data-toggle="tooltip" data-placement="top" title="New Comment">
                     <i className="icon-fw icon-bubble"></i> POST
                   </li>
                 )}

+ 2 - 2
src/client/js/components/Admin/Notification/ManageGlobalNotification.jsx

@@ -242,12 +242,12 @@ class ManageGlobalNotification extends React.Component {
               </div>
               <div className="my-1">
                 <TriggerEventCheckBox
-                  checkbox="warning"
+                  checkbox="pink"
                   event="pageMove"
                   checked={this.state.triggerEvents.has('pageMove')}
                   onChange={() => this.onChangeTriggerEvents('pageMove')}
                 >
-                  <span className="badge badge-pill badge-warning">
+                  <span className="badge badge-pill badge-pink">
                     <i className="icon-action-redo mr-1" />MOVE
                   </span>
                 </TriggerEventCheckBox>

+ 13 - 17
src/client/js/components/Admin/Security/SecuritySetting.jsx

@@ -118,7 +118,7 @@ class SecuritySetting extends React.Component {
           <div className="col-md-3 text-md-right py-2">
             <strong>{t('security_setting.Guest Users Access')}</strong>
           </div>
-          <div className="col-md-6 ml-md-5">
+          <div className="col-md-9">
             <div className="dropdown">
               <button
                 className={`btn btn-outline-secondary dropdown-toggle text-right col-12
@@ -143,30 +143,26 @@ class SecuritySetting extends React.Component {
                 </a>
               </div>
             </div>
-          </div>
-        </div>
-        {adminGeneralSecurityContainer.isWikiModeForced && (
-        <div className="row mb-4">
-          <div className="offset-3 col-6 text-left">
-            <p className="alert alert-warning mt-2 text-left">
-              <i className="icon-exclamation icon-fw">
-              </i><b>FIXED</b><br />
-              <b
-                dangerouslySetInnerHTML={{
+            {adminGeneralSecurityContainer.isWikiModeForced && (
+              <p className="alert alert-warning mt-2 text-left offset-3 col-6">
+                <i className="icon-exclamation icon-fw">
+                </i><b>FIXED</b><br />
+                <b
+                  dangerouslySetInnerHTML={{
                     __html: t('security_setting.Fixed by env var',
-                    { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
-                    }}
-              />
-            </p>
+                      { forcewikimode: 'FORCE_WIKI_MODE', wikimode: adminGeneralSecurityContainer.state.wikiMode }),
+                  }}
+                />
+              </p>
+            )}
           </div>
         </div>
-          )}
 
         <div className="row mb-4">
           <div className="col-md-3 text-md-right mb-2">
             <strong>{t('security_setting.complete_deletion')}</strong>
           </div>
-          <div className="col-md-6 ml-md-5">
+          <div className="col-md-6">
             <div className="dropdown">
               <button
                 className="btn btn-outline-secondary dropdown-toggle text-right col-12 col-md-auto"

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

@@ -99,7 +99,7 @@ class UserManagement extends React.Component {
           type="checkbox"
           id={`c_${status}`}
           checked={this.props.adminUsersContainer.isSelected(status)}
-          onClick={() => { this.handleClick(status) }}
+          onChange={() => { this.handleClick(status) }}
         />
         <label className="custom-control-label" htmlFor={`c_${status}`}>
           <span className={`badge badge-pill badge-${statusColor} d-inline-block vt mt-1`}>

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

@@ -99,11 +99,9 @@ class ExternalAccountTable extends React.Component {
                       </button>
                       <ul className="dropdown-menu" role="menu">
                         <li className="dropdown-header">{t('admin:user_management.user_table.edit_menu')}</li>
-                        <li>
-                          <a role="button" onClick={() => { return this.removeExtenalAccount(ea._id) }}>
-                            <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
-                          </a>
-                        </li>
+                        <a className="dropdown-item" role="button" onClick={() => { return this.removeExtenalAccount(ea._id) }}>
+                          <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
+                        </a>
                       </ul>
                     </div>
                   </td>

+ 1 - 1
src/client/js/components/Admin/Users/RemoveAdminButton.jsx

@@ -32,7 +32,7 @@ class RemoveAdminButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" href="" onClick={() => { this.onClickRemoveAdminBtn() }}>
+      <a className="dropdown-item" onClick={() => { this.onClickRemoveAdminBtn() }}>
         <i className="icon-fw icon-user-unfollow"></i> {t('admin:user_management.user_table.remove_admin_access')}
       </a>
     );

+ 1 - 1
src/client/js/components/Admin/Users/StatusActivateButton.jsx

@@ -31,7 +31,7 @@ class StatusActivateButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" href="" onClick={() => { this.onClickAcceptBtn() }}>
+      <a className="dropdown-item" onClick={() => { this.onClickAcceptBtn() }}>
         <i className="icon-fw icon-user-following"></i> {t('admin:user_management.user_table.accept')}
       </a>
     );

+ 1 - 1
src/client/js/components/Admin/Users/StatusSuspendedButton.jsx

@@ -31,7 +31,7 @@ class StatusSuspendedButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" href="" onClick={() => { this.onClickDeactiveBtn() }}>
+      <a className="dropdown-item" onClick={() => { this.onClickDeactiveBtn() }}>
         <i className="icon-fw icon-ban"></i> {t('admin:user_management.user_table.deactivate_account')}
       </a>
     );

+ 1 - 1
src/client/js/components/Admin/Users/UserRemoveButton.jsx

@@ -32,7 +32,7 @@ class UserRemoveButton extends React.Component {
     const { t } = this.props;
 
     return (
-      <a className="dropdown-item" href="" onClick={() => { this.onClickDeleteBtn() }}>
+      <a className="dropdown-item" onClick={() => { this.onClickDeleteBtn() }}>
         <i className="icon-fw icon-fire text-danger"></i> {t('Delete')}
       </a>
     );

+ 90 - 0
src/client/js/components/CreateTemplateModal.jsx

@@ -0,0 +1,90 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+import { pathUtils } from 'growi-commons';
+import urljoin from 'url-join';
+import { createSubscribedElement } from './UnstatedUtils';
+
+import PageContainer from '../services/PageContainer';
+
+const CreateTemplateModal = (props) => {
+  const { t, pageContainer } = props;
+
+  const { path } = pageContainer.state;
+  const parentPath = pathUtils.addTrailingSlash(path);
+
+  function generateUrl(label) {
+    return encodeURI(urljoin(parentPath, label, '#edit'));
+  }
+
+  /**
+   * @param {string} target Which hierarchy to create [children, decendants]
+   */
+  function renderTemplateCard(target, label) {
+    return (
+      <div className="card card-select-template">
+        <div className="card-header">{ t(`template.${target}.label`) }</div>
+        <div className="card-body">
+          <p className="text-center"><code>{label}</code></p>
+          <p className="form-text text-muted text-center"><small>{t(`template.${target}.desc`) }</small></p>
+        </div>
+        <div className="card-footer text-center">
+          <a
+            href={generateUrl(label)}
+            className="btn btn-sm btn-primary"
+            id={`template-button-${target}`}
+          >
+            { t('Edit') }
+          </a>
+        </div>
+      </div>
+    );
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
+        {t('template.modal_label.Create/Edit Template Page')}
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label className="mb-4">
+            <code>{parentPath}</code><br />
+            { t('template.modal_label.Create template under') }
+          </label>
+          <div className="row">
+            <div className="col-md-6">
+              {renderTemplateCard('children', '_template')}
+            </div>
+            <div className="col-md-6">
+              {renderTemplateCard('decendants', '__template')}
+            </div>
+          </div>
+        </div>
+      </ModalBody>
+    </Modal>
+
+  );
+};
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const CreateTemplateModalWrapper = (props) => {
+  return createSubscribedElement(CreateTemplateModal, props, [PageContainer]);
+};
+
+
+CreateTemplateModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(CreateTemplateModalWrapper);

+ 73 - 0
src/client/js/components/EmptyTrashModal.jsx

@@ -0,0 +1,73 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+import { createSubscribedElement } from './UnstatedUtils';
+
+import AppContainer from '../services/AppContainer';
+import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const EmptyTrashModal = (props) => {
+  const {
+    t, isOpen, onClose, appContainer,
+  } = props;
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  async function emptyTrash() {
+    setErrorCode(null);
+    setErrorMessage(null);
+    try {
+      await appContainer.apiv3Delete('/pages/empty-trash');
+      window.location.reload();
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  function emptyButtonHandler() {
+    emptyTrash();
+  }
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={onClose} className="bg-danger text-light">
+        { t('modal_empty.empty_the_trash')}
+      </ModalHeader>
+      <ModalBody>
+        { t('modal_empty.notice')}
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} />
+        <button type="button" className="btn btn-danger" onClick={emptyButtonHandler}>
+          <i className="icon-trash mr-2" aria-hidden="true"></i>Empty
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const EmptyTrashModalWrapper = (props) => {
+  return createSubscribedElement(EmptyTrashModal, props, [AppContainer]);
+};
+
+
+EmptyTrashModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+  onClickEmptyBtn: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(EmptyTrashModalWrapper);

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

@@ -150,10 +150,10 @@ class InstallerForm extends React.Component {
             <input type="hidden" name="_csrf" value={this.props.csrf} />
 
             <div className="input-group mt-4 mb-3 d-flex justify-content-center">
-              <button type="submit" className="btn-fill btn btn-register px-0 py-2" id="register">
+              <button type="submit" className="btn-fill btn btn-register" id="register">
                 <div className="eff"></div>
-                <span className="btn-label p-3"><i className="icon-user-follow" /></span>
-                <span className="btn-label-text p-3">{ this.props.t('Create') }</span>
+                <span className="btn-label"><i className="icon-user-follow" /></span>
+                <span className="btn-label-text">{ this.props.t('Create') }</span>
               </button>
             </div>
 

+ 316 - 0
src/client/js/components/LoginForm.jsx

@@ -0,0 +1,316 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import ReactCardFlip from 'react-card-flip';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+import NoLoginContainer from '../services/NoLoginContainer';
+
+class LoginForm extends React.Component {
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isRegistering: false,
+    };
+
+    this.switchForm = this.switchForm.bind(this);
+    this.handleLoginWithExternalAuth = this.handleLoginWithExternalAuth.bind(this);
+    this.renderLocalOrLdapLoginForm = this.renderLocalOrLdapLoginForm.bind(this);
+    this.renderExternalAuthLoginForm = this.renderExternalAuthLoginForm.bind(this);
+    this.renderExternalAuthInput = this.renderExternalAuthInput.bind(this);
+    this.renderRegisterForm = this.renderRegisterForm.bind(this);
+
+    const { hash } = window.location;
+    if (hash === '#register') {
+      this.state.isRegistering = true;
+    }
+  }
+
+  switchForm() {
+    this.setState({ isRegistering: !this.state.isRegistering });
+  }
+
+  handleLoginWithExternalAuth(e) {
+    const auth = e.currentTarget.id;
+    const csrf = this.props.noLoginContainer.csrfToken;
+    window.location.href = `/passport/${auth}?_csrf=${csrf}`;
+  }
+
+  renderLocalOrLdapLoginForm() {
+    const { t, noLoginContainer, isLdapStrategySetup } = this.props;
+
+    return (
+      <form role="form" action="/login" method="post">
+        <div className="input-group">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-user"></i>
+            </span>
+          </div>
+          <input type="text" className="form-control rounded-0" placeholder="Username or E-mail" name="loginForm[username]" />
+          {isLdapStrategySetup && (
+            <div className="input-group-append">
+              <small className="input-group-text text-success">
+                <i className="icon-fw icon-check"></i> LDAP
+              </small>
+            </div>
+          )}
+        </div>
+
+        <div className="input-group">
+          <div className="input-group-prepend">
+            <span className="input-group-text">
+              <i className="icon-lock"></i>
+            </span>
+          </div>
+          <input type="password" className="form-control rounded-0" placeholder="Password" name="loginForm[password]" />
+        </div>
+
+        <div className="input-group my-4">
+          <input type="hidden" name="_csrf" value={noLoginContainer.csrfToken} />
+          <button type="submit" id="login" className="btn btn-fill rounded-0 login mx-auto">
+            <div className="eff"></div>
+            <span className="btn-label">
+              <i className="icon-login"></i>
+            </span>
+            <span className="btn-label-text">{t('Sign in')}</span>
+          </button>
+        </div>
+      </form>
+    );
+  }
+
+  renderExternalAuthInput(auth) {
+    const { t } = this.props;
+    const authIconNames = {
+      google: 'google',
+      github: 'github',
+      facebook: 'facebook',
+      twitter: 'twitter',
+      oidc: 'openid',
+      saml: 'key',
+      basic: 'lock',
+    };
+
+    return (
+      <div key={auth} className="col-6 my-2">
+        <button type="button" className="btn btn-fill rounded-0" id={auth} onClick={this.handleLoginWithExternalAuth}>
+          <div className="eff"></div>
+          <span className="btn-label">
+            <i className={`fa fa-${authIconNames[auth]}`}></i>
+          </span>
+          <span className="btn-label-text">{t('Sign in')}</span>
+        </button>
+        <div className="small text-right">by {auth} Account</div>
+      </div>
+    );
+  }
+
+  renderExternalAuthLoginForm() {
+    const { isLocalStrategySetup, isLdapStrategySetup, objOfIsExternalAuthEnableds } = this.props;
+    const isExternalAuthCollapsible = isLocalStrategySetup || isLdapStrategySetup;
+    const collapsibleClass = isExternalAuthCollapsible ? 'collapse collapse-external-auth' : '';
+
+    return (
+      <>
+        <div className="border-top border-bottom">
+          <div id="external-auth" className={`external-auth ${collapsibleClass}`}>
+            <div className="row mt-2">
+              {Object.keys(objOfIsExternalAuthEnableds).map((auth) => {
+                if (!objOfIsExternalAuthEnableds[auth]) {
+                  return;
+                }
+                return this.renderExternalAuthInput(auth);
+              })}
+            </div>
+          </div>
+        </div>
+        <div className="text-center">
+          <button
+            type="button"
+            className="btn btn-secondary btn-sm rounded-0 mb-3"
+            data-toggle={isExternalAuthCollapsible ? 'collapse' : ''}
+            data-target="#external-auth"
+            aria-expanded="false"
+            aria-controls="external-auth"
+          >
+            External Auth
+          </button>
+        </div>
+      </>
+    );
+  }
+
+  renderRegisterForm() {
+    const {
+      t,
+      username,
+      name,
+      email,
+      noLoginContainer,
+      registrationMode,
+      registrationWhiteList,
+    } = this.props;
+
+    return (
+      <React.Fragment>
+        {registrationMode === 'Restricted' && (
+        <p className="alert alert-warning">
+          {t('page_register.notice.restricted')}
+          <br />
+          {t('page_register.notice.restricted_defail')}
+        </p>
+        )}
+        <form role="form" action="/register" method="post" id="register-form">
+          <div className="input-group" id="input-group-username">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-user"></i>
+              </span>
+            </div>
+            <input type="text" className="form-control rounded-0" placeholder={t('User ID')} name="registerForm[username]" defaultValue={username} required />
+          </div>
+          <p className="form-text text-danger">
+            <span id="help-block-username"></span>
+          </p>
+
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-tag"></i>
+              </span>
+            </div>
+            <input type="text" className="form-control rounded-0" placeholder={t('Name')} name="registerForm[name]" defaultValue={name} required />
+          </div>
+
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-envelope"></i>
+              </span>
+            </div>
+            <input type="email" className="form-control rounded-0" placeholder={t('Email')} name="registerForm[email]" defaultValue={email} required />
+          </div>
+
+          {registrationWhiteList.length > 0 && (
+          <>
+            <p className="form-text">{t('page_register.form_help.email')}</p>
+            <ul>
+              {registrationWhiteList.map((elem) => {
+                  return (
+                    <li key={elem}>
+                      <code>{elem}</code>
+                    </li>
+                  );
+                })}
+            </ul>
+          </>
+          )}
+
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">
+                <i className="icon-lock"></i>
+              </span>
+            </div>
+            <input type="password" className="form-control rounded-0" placeholder={t('Password')} name="registerForm[password]" required />
+          </div>
+
+          <div className="input-group justify-content-center my-4">
+            <input type="hidden" name="_csrf" value={noLoginContainer.csrfToken} />
+            <button type="submit" className="btn btn-fill rounded-0" id="register">
+              <div className="eff"></div>
+              <span className="btn-label">
+                <i className="icon-user-follow"></i>
+              </span>
+              <span className="btn-label-text">{t('Sign up')}</span>
+            </button>
+          </div>
+        </form>
+
+        <div className="border-bottom"></div>
+
+        <div className="row">
+          <div className="text-right col-12 mt-2 py-2">
+            <a href="#login" id="login" className="link-switch" onClick={this.switchForm}>
+              <i className="icon-fw icon-login"></i>
+              {t('Sign in is here')}
+            </a>
+          </div>
+        </div>
+      </React.Fragment>
+    );
+  }
+
+  render() {
+    const {
+      t,
+      isLocalStrategySetup,
+      isLdapStrategySetup,
+      isRegistrationEnabled,
+      objOfIsExternalAuthEnableds,
+    } = this.props;
+
+    const isLocalOrLdapStrategiesEnabled = isLocalStrategySetup || isLdapStrategySetup;
+    const isSomeExternalAuthEnabled = Object.values(objOfIsExternalAuthEnableds).some(elem => elem);
+
+    return (
+      <div className="login-dialog mx-auto" id="login-dialog">
+        <div className="row mx-0">
+          <div className="col-12">
+            <ReactCardFlip isFlipped={this.state.isRegistering} flipDirection="horizontal" cardZIndex="3">
+              <div className="front">
+                {isLocalOrLdapStrategiesEnabled && this.renderLocalOrLdapLoginForm()}
+                {isSomeExternalAuthEnabled && this.renderExternalAuthLoginForm()}
+                {isRegistrationEnabled && (
+                  <div className="row">
+                    <div className="col-12 text-right py-2">
+                      <a href="#register" id="register" className="link-switch" onClick={this.switchForm}>
+                        <i className="ti-check-box"></i> {t('Sign up is here')}
+                      </a>
+                    </div>
+                  </div>
+              )}
+              </div>
+              <div className="back">
+                {isRegistrationEnabled && this.renderRegisterForm()}
+              </div>
+            </ReactCardFlip>
+          </div>
+        </div>
+        <a href="https://growi.org" className="link-growi-org pl-3">
+          <span className="growi">GROWI</span>.<span className="org">ORG</span>
+        </a>
+      </div>
+    );
+  }
+
+}
+
+/**
+ * Wrapper component for using unstated
+ */
+const LoginFormWrapper = (props) => {
+  return createSubscribedElement(LoginForm, props, [NoLoginContainer]);
+};
+
+LoginForm.propTypes = {
+  // i18next
+  t: PropTypes.func.isRequired,
+  noLoginContainer: PropTypes.instanceOf(NoLoginContainer).isRequired,
+  isRegistering: PropTypes.bool,
+  username: PropTypes.string,
+  name: PropTypes.string,
+  email: PropTypes.string,
+  isRegistrationEnabled: PropTypes.bool,
+  registrationMode: PropTypes.string,
+  registrationWhiteList: PropTypes.array,
+  isLocalStrategySetup: PropTypes.bool,
+  isLdapStrategySetup: PropTypes.bool,
+  objOfIsExternalAuthEnableds: PropTypes.object,
+};
+
+export default withTranslation()(LoginFormWrapper);

+ 3 - 5
src/client/js/components/Me/BasicInfoSettings.jsx

@@ -68,17 +68,15 @@ class BasicInfoSettings extends React.Component {
               defaultValue={personalContainer.state.email}
               onChange={(e) => { personalContainer.changeEmail(e.target.value) }}
             />
-          </div>
-          {registrationWhiteList.length !== 0 && (
-            <div className="col-sm-offset-2 col-sm-10">
+            {registrationWhiteList.length !== 0 && (
               <div className="form-text text-muted">
                 {t('page_register.form_help.email')}
                 <ul>
                   {registrationWhiteList.map(data => <li key={data}><code>{data}</code></li>)}
                 </ul>
               </div>
-            </div>
-          )}
+            )}
+          </div>
         </div>
 
         <div className="form-group row">

+ 1 - 1
src/client/js/components/Me/ExternalAccountRow.jsx

@@ -18,7 +18,7 @@ const ExternalAccountRow = (props) => {
       <td className="text-center">
         <button
           type="button"
-          className="btn btn-outline-secondary btn-sm btn-danger"
+          className="btn btn-sm btn-danger"
           onClick={() => props.openDisassociateModal(account)}
         >
           <i className="ti-unlink"></i>

+ 1 - 2
src/client/js/components/Me/PersonalSettings.jsx

@@ -15,7 +15,6 @@ class PersonalSettings extends React.Component {
 
     return (
       <Fragment>
-        {/* TODO GW-226 adapt BS4 */}
         <div className="m-t-10">
           <div className="personal-settings">
             <ul className="nav nav-tabs" role="tablist">
@@ -24,7 +23,7 @@ class PersonalSettings extends React.Component {
               </li>
               <li className="nav-item">
                 <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab">
-                  <i className="icon-share-alt"></i>
+                  <i className="icon-share-alt mr-1"></i>
                   { t('admin:user_management.external_accounts') }
                 </a>
               </li>

+ 3 - 13
src/client/js/components/Navbar/GrowiSubNavigation.jsx

@@ -61,7 +61,7 @@ const GrowiSubNavigation = (props) => {
   const isPageForbidden = document.querySelector('#grw-subnav').getAttribute('data-is-forbidden-page') === 'true';
   const { appContainer, pageContainer } = props;
   const {
-    pageId, path, createdAt, creator, updatedAt, revisionAuthor, isHeaderSticky, isSubnavCompact,
+    pageId, path, createdAt, creator, updatedAt, revisionAuthor,
   } = pageContainer.state;
 
   const isPageNotFound = pageId == null;
@@ -77,16 +77,6 @@ const GrowiSubNavigation = (props) => {
   }
 
   const additionalClassNames = ['grw-subnavbar'];
-  const layoutType = appContainer.getConfig().layoutType;
-
-  if (layoutType === 'growi') {
-    if (isHeaderSticky) {
-      additionalClassNames.push('grw-subnavbar-sticky');
-    }
-    if (isSubnavCompact) {
-      additionalClassNames.push('grw-subnavbar-compact');
-    }
-  }
 
   return (
     <div className={`d-flex align-items-center justify-content-between px-3 py-1 ${additionalClassNames.join(' ')}`}>
@@ -116,12 +106,12 @@ const GrowiSubNavigation = (props) => {
         <ul className="authors text-nowrap d-none d-lg-block d-edit-none">
           { creator != null && (
             <li>
-              <PageCreator creator={creator} createdAt={createdAt} isCompactMode={isSubnavCompact} />
+              <PageCreator creator={creator} createdAt={createdAt} />
             </li>
           ) }
           { revisionAuthor != null && (
             <li className="mt-1">
-              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} isCompactMode={isSubnavCompact} />
+              <RevisionAuthor revisionAuthor={revisionAuthor} updatedAt={updatedAt} />
             </li>
           ) }
         </ul>

+ 2 - 10
src/client/js/components/Navbar/GrowiSubNavigationForUserPage.jsx

@@ -36,22 +36,14 @@ const GrowiSubNavigationForUserPage = (props) => {
   const pageUser = JSON.parse(document.querySelector('#grw-subnav-for-user-page').getAttribute('data-page-user'));
   const { appContainer, pageContainer } = props;
   const {
-    pageId, path, isHeaderSticky, isSubnavCompact,
+    pageId, path,
   } = pageContainer.state;
 
   const additionalClassNames = ['grw-subnavbar', 'grw-subnavbar-user-page'];
   const layoutType = appContainer.getConfig().layoutType;
 
   if (layoutType === 'growi') {
-    if (isHeaderSticky) {
-      additionalClassNames.push('grw-subnavbar-sticky');
-    }
-    if (isSubnavCompact) {
-      additionalClassNames.push('py-2 grw-subnavbar-compact');
-    }
-    else {
-      additionalClassNames.push('py-3');
-    }
+    additionalClassNames.push('py-3');
   }
 
   return (

+ 0 - 5
src/client/js/components/Navbar/PersonalDropdown.jsx

@@ -70,11 +70,6 @@ const PersonalDropdown = (props) => {
 
         <div className="dropdown-divider"></div>
 
-        <a className="dropdown-item" href={`/user/${user.username}#user-draft-list`}><i className="icon-fw icon-docs"></i>{ t('List Drafts') }</a>
-        <a className="dropdown-item" href="/trash"><i className="icon-fw icon-trash"></i>{ t('Deleted Pages') }</a>
-
-        <div className="dropdown-divider"></div>
-
         <h6 className="dropdown-header">Color Scheme</h6>
         <form className="px-4">
           <div className="form-row align-items-center">

+ 42 - 15
src/client/js/components/HeaderSearchBox.jsx → src/client/js/components/Navbar/SearchTop.jsx

@@ -2,13 +2,13 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { createSubscribedElement } from './UnstatedUtils';
-import AppContainer from '../services/AppContainer';
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
 
-import SearchForm from './SearchForm';
+import SearchForm from '../SearchForm';
 
 
-class HeaderSearchBox extends React.Component {
+class SearchTop extends React.Component {
 
   constructor(props) {
     super(props);
@@ -16,6 +16,7 @@ class HeaderSearchBox extends React.Component {
     this.state = {
       text: '',
       isScopeChildren: false,
+      isCollapsed: true,
     };
 
     this.onInputChange = this.onInputChange.bind(this);
@@ -24,10 +25,14 @@ class HeaderSearchBox extends React.Component {
     this.search = this.search.bind(this);
   }
 
-  componentDidMount() {
+  componentWillMount() {
+    this.initBreakpointEvents();
   }
 
-  componentWillUnmount() {
+  initBreakpointEvents() {
+    this.props.appContainer.addBreakpointListener('md', (mql) => {
+      this.setState({ isCollapsed: !mql.matches });
+    }, true);
   }
 
   onInputChange(text) {
@@ -56,7 +61,23 @@ class HeaderSearchBox extends React.Component {
     window.location.href = url.href;
   }
 
-  render() {
+  Root = ({ children }) => {
+    const { isCollapsed } = this.state;
+
+    return isCollapsed
+      ? (
+        <div id="grw-search-top-collapse" className="collapse bg-dark p-3">
+          {children}
+        </div>
+      )
+      : (
+        <div className="grw-search-top-fixed position-fixed">
+          {children}
+        </div>
+      );
+  };
+
+  SearchTopForm = () => {
     const { t, appContainer } = this.props;
     const scopeLabel = this.state.isScopeChildren
       ? t('header_search_box.label.This tree')
@@ -94,19 +115,25 @@ class HeaderSearchBox extends React.Component {
     );
   }
 
+  render() {
+    const { Root, SearchTopForm } = this;
+    return (
+      <Root><SearchTopForm /></Root>
+    );
+  }
+
 }
 
+SearchTop.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
 
 /**
  * Wrapper component for using unstated
  */
-const HeaderSearchBoxWrapper = (props) => {
-  return createSubscribedElement(HeaderSearchBox, props, [AppContainer]);
-};
-
-HeaderSearchBox.propTypes = {
-  t: PropTypes.func.isRequired, // i18next
-  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+const SearchTopWrapper = (props) => {
+  return createSubscribedElement(SearchTop, props, [AppContainer]);
 };
 
-export default withTranslation()(HeaderSearchBoxWrapper);
+export default withTranslation()(SearchTopWrapper);

+ 150 - 0
src/client/js/components/Page/PageManagement.jsx

@@ -0,0 +1,150 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+import { isTopPage } from '@commons/util/path-utils';
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import PageContainer from '../../services/PageContainer';
+import PageDeleteModal from '../PageDeleteModal';
+import PageRenameModal from '../PageRenameModal';
+import PageDuplicateModal from '../PageDuplicateModal';
+import CreateTemplateModal from '../CreateTemplateModal';
+
+
+const PageManagement = (props) => {
+  const { t, appContainer, pageContainer } = props;
+  const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
+
+  const { currentUser } = appContainer;
+  const isTopPagePath = isTopPage(path);
+
+  const [isPageRenameModalShown, setIsPageRenameModalShown] = useState(false);
+  const [isPageDuplicateModalShown, setIsPageDuplicateModalShown] = useState(false);
+  const [isPageTemplateModalShown, setIsPageTempleteModalShown] = useState(false);
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
+
+  function openPageRenameModalHandler() {
+    setIsPageRenameModalShown(true);
+  }
+
+  function closePageRenameModalHandler() {
+    setIsPageRenameModalShown(false);
+  }
+
+  function openPageDuplicateModalHandler() {
+    setIsPageDuplicateModalShown(true);
+  }
+
+  function closePageDuplicateModalHandler() {
+    setIsPageDuplicateModalShown(false);
+  }
+
+  function openPageTemplateModalHandler() {
+    setIsPageTempleteModalShown(true);
+  }
+
+  function closePageTemplateModalHandler() {
+    setIsPageTempleteModalShown(false);
+  }
+
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function closePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
+
+
+  function renderDropdownItemForNotTopPage() {
+    return (
+      <>
+        <a className="dropdown-item" type="button" onClick={openPageRenameModalHandler}>
+          <i className="icon-fw icon-action-redo"></i> { t('Move/Rename') }
+        </a>
+        <a className="dropdown-item" type="button" onClick={openPageDuplicateModalHandler}>
+          <i className="icon-fw icon-docs"></i> { t('Duplicate') }
+        </a>
+        <div className="dropdown-divider"></div>
+      </>
+    );
+  }
+
+  function renderDropdownItemForDeletablePage() {
+    return (
+      <>
+        <div className="dropdown-divider"></div>
+        <a className="dropdown-item" type="button" onClick={openPageDeleteModalHandler}>
+          <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
+        </a>
+      </>
+    );
+  }
+
+  function renderModals() {
+    return (
+      <>
+        <PageRenameModal
+          isOpen={isPageRenameModalShown}
+          onClose={closePageRenameModalHandler}
+          path={path}
+        />
+        <PageDuplicateModal
+          isOpen={isPageDuplicateModalShown}
+          onClose={closePageDuplicateModalHandler}
+        />
+        <CreateTemplateModal
+          isOpen={isPageTemplateModalShown}
+          onClose={closePageTemplateModalHandler}
+        />
+        <PageDeleteModal
+          isOpen={isPageDeleteModalShown}
+          onClose={closePageDeleteModalHandler}
+          path={path}
+          isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+        />
+      </>
+    );
+  }
+
+  return (
+    <>
+      <a
+        role="button"
+        className={`nav-link dropdown-toggle dropdown-toggle-no-caret ${currentUser == null && 'dropdown-toggle-disabled'}`}
+        href="#"
+        data-toggle={`${currentUser == null ? 'tooltip' : 'dropdown'}`}
+        data-placement="top"
+        data-container="body"
+        title={t('Not available for guest')}
+      >
+        <i className="icon-options-vertical"></i>
+      </a>
+      <div className="dropdown-menu dropdown-menu-right">
+        {!isTopPagePath && renderDropdownItemForNotTopPage()}
+        <a className="dropdown-item" type="button" onClick={openPageTemplateModalHandler}>
+          <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
+        </a>
+        {(!isTopPagePath && isDeletable) && renderDropdownItemForDeletablePage()}
+      </div>
+      {renderModals()}
+    </>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageManagementWrapper = (props) => {
+  return createSubscribedElement(PageManagement, props, [AppContainer, PageContainer]);
+};
+
+
+PageManagement.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(PageManagementWrapper);

+ 138 - 0
src/client/js/components/Page/TrashPageAlert.jsx

@@ -0,0 +1,138 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+import PageContainer from '../../services/PageContainer';
+import UserPicture from '../User/UserPicture';
+import PutbackPageModal from '../PutbackPageModal';
+import EmptyTrashModal from '../EmptyTrashModal';
+import PageDeleteModal from '../PageDeleteModal';
+
+
+const TrashPageAlert = (props) => {
+  const { t, appContainer, pageContainer } = props;
+  const {
+    path, isDeleted, revisionAuthor, updatedAt, hasChildren, isAbleToDeleteCompletely,
+  } = pageContainer.state;
+  const { currentUser } = appContainer;
+  const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
+  const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
+
+  function openEmptyTrashModalHandler() {
+    setIsEmptyTrashModalShown(true);
+  }
+
+  function closeEmptyTrashModalHandler() {
+    setIsEmptyTrashModalShown(false);
+  }
+
+  function openPutbackPageModalHandler() {
+    setIsPutbackPageModalShown(true);
+  }
+
+  function closePutbackPageModalHandler() {
+    setIsPutbackPageModalShown(false);
+  }
+
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function opclosePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
+
+  function renderEmptyButton() {
+    return (
+      <button
+        href="#"
+        type="button"
+        className="btn btn-danger rounded-pill btn-sm ml-auto"
+        data-target="#emptyTrash"
+        onClick={openEmptyTrashModalHandler}
+      >
+        <i className="icon-trash" aria-hidden="true"></i>{ t('modal_empty.empty_the_trash') }
+      </button>
+    );
+  }
+
+  function renderTrashPageManagementButtons() {
+    return (
+      <>
+        <button
+          type="button"
+          className="btn btn-info rounded-pill btn-sm ml-auto mr-2"
+          onClick={openPutbackPageModalHandler}
+          data-toggle="modal"
+        >
+          <i className="icon-action-undo" aria-hidden="true"></i> { t('Put Back') }
+        </button>
+        <button
+          type="button"
+          className="btn btn-danger rounded-pill btn-sm mr-2"
+          disabled={!isAbleToDeleteCompletely}
+          onClick={openPageDeleteModalHandler}
+        >
+          <i className="icon-fire" aria-hidden="true"></i> { t('Delete Completely') }
+        </button>
+      </>
+    );
+  }
+
+  function renderModals() {
+    return (
+      <>
+        <EmptyTrashModal
+          isOpen={isEmptyTrashModalShown}
+          onClose={closeEmptyTrashModalHandler}
+        />
+        <PutbackPageModal
+          isOpen={isPutbackPageModalShown}
+          onClose={closePutbackPageModalHandler}
+          path={path}
+        />
+        <PageDeleteModal
+          isOpen={isPageDeleteModalShown}
+          onClose={opclosePageDeleteModalHandler}
+          path={path}
+          isDeleteCompletelyModal
+          isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+        />
+      </>
+    );
+  }
+
+  return (
+    <>
+      <div className="alert alert-warning py-3 px-4 d-flex align-items-center">
+        <div>
+          This page is in the trash <i className="icon-trash" aria-hidden="true"></i>.
+          {isDeleted && <span><br /><UserPicture user={revisionAuthor} /> Deleted by {revisionAuthor.name} at {updatedAt}</span>}
+        </div>
+        {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
+        {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}
+      </div>
+      {renderModals()}
+    </>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const TrashPageAlertWrapper = (props) => {
+  return createSubscribedElement(TrashPageAlert, props, [AppContainer, PageContainer]);
+};
+
+
+TrashPageAlert.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(TrashPageAlertWrapper);

+ 88 - 58
src/client/js/components/PageCreateModal.jsx

@@ -13,17 +13,16 @@ import { pathUtils } from 'growi-commons';
 import { createSubscribedElement } from './UnstatedUtils';
 
 import AppContainer from '../services/AppContainer';
-import PageContainer from '../services/PageContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
 
 const PageCreateModal = (props) => {
-  const { t, appContainer, pageContainer } = props;
+  const { t, appContainer } = props;
 
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
-  const { path } = pageContainer.state;
+  const { pathname } = window.location;
   const userPageRootPath = userPageRoot(appContainer.currentUser);
-  const parentPath = pathUtils.addTrailingSlash(path);
+  const parentPath = pathUtils.addTrailingSlash(pathname);
   const now = format(new Date(), 'yyyy/MM/dd');
 
   const [todayInput1, setTodayInput1] = useState(t('Memo'));
@@ -81,6 +80,14 @@ const PageCreateModal = (props) => {
     window.location.href = encodeURI(urljoin(pageNameInput, '#edit'));
   }
 
+  function ppacInputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  function ppacSubmitHandler() {
+    createInputPage();
+  }
+
   /**
    * access template page
    */
@@ -91,34 +98,41 @@ const PageCreateModal = (props) => {
 
   function renderCreateTodayForm() {
     return (
-      <div className="row form-group">
+      <div className="row">
         <fieldset className="col-12 mb-4">
           <h3 className="grw-modal-head pb-2">{ t("Create today's") }</h3>
-          <div className="d-flex">
-            <div className="create-page-input-row d-flex align-items-center">
-              <span>{userPageRootPath}/</span>
-              <input
-                type="text"
-                className="page-today-input1 form-control text-center"
-                value={todayInput1}
-                onChange={e => onChangeTodayInput1Handler(e.target.value)}
-              />
-              <span className="page-today-suffix">/{now}/</span>
+
+          <div className="d-sm-flex align-items-center justify-items-between">
+
+            <div className="d-flex align-items-center flex-fill flex-wrap flex-lg-nowrap">
+              <div className="d-flex align-items-center">
+                <span>{userPageRootPath}/</span>
+                <input
+                  type="text"
+                  className="page-today-input1 form-control text-center mx-2"
+                  value={todayInput1}
+                  onChange={e => onChangeTodayInput1Handler(e.target.value)}
+                />
+                <span className="page-today-suffix">/{now}/</span>
+              </div>
               <input
                 type="text"
-                className="page-today-input2 form-control"
+                className="page-today-input2 form-control mt-1 mt-lg-0 mx-lg-2 flex-fill"
                 id="page-today-input2"
                 placeholder={t('Input page name (optional)')}
                 value={todayInput2}
                 onChange={e => onChangeTodayInput2Handler(e.target.value)}
               />
             </div>
-            <div className="create-page-button-container">
-              <button type="button" className="btn btn-outline-primary rounded-pill" onClick={createTodayPage}>
+
+            <div className="d-flex justify-content-end mt-1 mt-sm-0">
+              <button type="button" className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3" onClick={createTodayPage}>
                 <i className="icon-fw icon-doc"></i>{ t('Create') }
               </button>
             </div>
+
           </div>
+
         </fieldset>
       </div>
     );
@@ -126,31 +140,43 @@ const PageCreateModal = (props) => {
 
   function renderInputPageForm() {
     return (
-      <div className="row form-group">
+      <div className="row">
         <fieldset className="col-12 mb-4">
           <h3 className="grw-modal-head pb-2">{ t('Create under') }</h3>
-          <div className="d-flex create-page-input-container">
-            <div className="create-page-input-row d-flex align-items-center">
+
+          <div className="d-sm-flex align-items-center justify-items-between">
+
+            <div className="flex-fill">
               {isReachable
-                // GW-2355 refactor typeahead
-                ? <PagePathAutoComplete crowi={appContainer} initializedPath={path} addTrailingSlash />
+                ? (
+                  <PagePathAutoComplete
+                    crowi={appContainer}
+                    initializedPath={pathname}
+                    addTrailingSlash
+                    onSubmit={ppacSubmitHandler}
+                    onInputChange={ppacInputChangeHandler}
+                  />
+                )
                 : (
                   <input
                     type="text"
                     value={pageNameInput}
-                    className="page-name-input form-control"
+                    className="form-control flex-fill"
                     placeholder={t('Input page name')}
                     onChange={e => onChangePageNameInputHandler(e.target.value)}
                     required
                   />
                 )}
             </div>
-            <div className="create-page-button-container">
-              <button type="submit" className="btn btn-outline-primary rounded-pill" onClick={createInputPage}>
+
+            <div className="d-flex justify-content-end mt-1 mt-sm-0">
+              <button type="button" className="grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3" onClick={createInputPage}>
                 <i className="icon-fw icon-doc"></i>{ t('Create') }
               </button>
             </div>
+
           </div>
+
         </fieldset>
       </div>
     );
@@ -158,53 +184,58 @@ const PageCreateModal = (props) => {
 
   function renderTemplatePageForm() {
     return (
-      <div className="row form-group">
+      <div className="row">
         <fieldset className="col-12">
+
           <h3 className="grw-modal-head pb-2">{ t('template.modal_label.Create template under')}<br />
-            <code>{path}</code>
+            <code>{pathname}</code>
           </h3>
-          <div className="d-flex create-page-input-container">
-            <div className="create-page-input-row d-flex align-items-center">
-
-              <div id="dd-template-type" className="dropdown w-100">
-                <button id="template-type" type="button" className="btn btn-secondary btn dropdown-toggle" data-toggle="dropdown">
-                  {template == null && t('template.option_label.select') }
-                  {template === 'children' && t('template.children.label')}
-                  {template === 'decendants' && t('template.decendants.label')}
-                </button>
-                <div className="dropdown-menu" aria-labelledby="userMenu">
-                  <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
-                    { t('template.children.label') } (_template)<br className="d-block d-md-none" />
-                    <small className="text-muted text-wrap">- { t('template.children.desc') }</small>
-                  </a>
-                  <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
-                    { t('template.decendants.label') } (__template) <br className="d-block d-md-none" />
-                    <small className="text-muted">- { t('template.decendants.desc') }</small>
-                  </a>
-                </div>
-              </div>
 
+          <div className="d-sm-flex align-items-center justify-items-between">
+
+            <div id="dd-template-type" className="dropdown flex-fill">
+              <button id="template-type" type="button" className="btn btn-secondary btn dropdown-toggle w-100" data-toggle="dropdown">
+                {template == null && t('template.option_label.select') }
+                {template === 'children' && t('template.children.label')}
+                {template === 'decendants' && t('template.decendants.label')}
+              </button>
+              <div className="dropdown-menu" aria-labelledby="userMenu">
+                <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('children')}>
+                  { t('template.children.label') } (_template)<br className="d-block d-md-none" />
+                  <small className="text-muted text-wrap">- { t('template.children.desc') }</small>
+                </a>
+                <a className="dropdown-item" type="button" onClick={() => onChangeTemplateHandler('decendants')}>
+                  { t('template.decendants.label') } (__template) <br className="d-block d-md-none" />
+                  <small className="text-muted">- { t('template.decendants.desc') }</small>
+                </a>
+              </div>
             </div>
-            <div className="create-page-button-container">
-              <button type="button" className={`btn btn-outline-primary rounded-pill ${template == null && 'disabled'}`} onClick={createTemplatePage}>
-                <i className="icon-fw icon-doc"></i>
-                <span>{ t('Edit') }</span>
+
+            <div className="d-flex justify-content-end mt-1 mt-sm-0">
+              <button
+                type="button"
+                className={`grw-btn-create-page btn btn-outline-primary rounded-pill text-nowrap ml-3 ${template == null && 'disabled'}`}
+                onClick={createTemplatePage}
+              >
+                <i className="icon-fw icon-doc"></i>{ t('Edit') }
               </button>
             </div>
+
           </div>
+
         </fieldset>
       </div>
     );
   }
   return (
-    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal}>
+    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal} className="grw-create-page">
       <ModalHeader tag="h4" toggle={appContainer.closePageCreateModal} className="bg-primary text-light">
         { t('New Page') }
       </ModalHeader>
       <ModalBody>
-        {renderCreateTodayForm}
-        {renderInputPageForm}
-        {renderTemplatePageForm}
+        {renderCreateTodayForm()}
+        {renderInputPageForm()}
+        {renderTemplatePageForm()}
       </ModalBody>
     </Modal>
 
@@ -216,14 +247,13 @@ const PageCreateModal = (props) => {
  * Wrapper component for using unstated
  */
 const ModalControlWrapper = (props) => {
-  return createSubscribedElement(PageCreateModal, props, [AppContainer, PageContainer]);
+  return createSubscribedElement(PageCreateModal, props, [AppContainer]);
 };
 
 
 PageCreateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 };
 
 export default withTranslation()(ModalControlWrapper);

+ 161 - 0
src/client/js/components/PageDeleteModal.jsx

@@ -0,0 +1,161 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+import PageContainer from '../services/PageContainer';
+
+import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const deleteIconAndKey = {
+  completely: {
+    color: 'danger',
+    icon: 'fire',
+    translationKey: 'completely',
+  },
+  temporary: {
+    color: 'primary',
+    icon: 'trash',
+    translationKey: 'page',
+  },
+};
+
+const PageDeleteModal = (props) => {
+  const {
+    t, pageContainer, isOpen, onClose, isDeleteCompletelyModal, path, isAbleToDeleteCompletely,
+  } = props;
+  const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
+  const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
+  const deleteMode = isDeleteCompletely ? 'completely' : 'temporary';
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  function changeIsDeleteRecursivelyHandler() {
+    setIsDeleteRecursively(!isDeleteRecursively);
+  }
+
+  function changeIsDeleteCompletelyHandler() {
+    if (!isAbleToDeleteCompletely) {
+      return;
+    }
+    setIsDeleteCompletely(!isDeleteCompletely);
+  }
+
+  async function deletePage() {
+    setErrorCode(null);
+    setErrorMessage(null);
+
+    try {
+      const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
+      const trashPagePath = response.page.path;
+      window.location.href = encodeURI(trashPagePath);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  async function deleteButtonHandler() {
+    deletePage();
+  }
+
+  function renderDeleteRecursivelyForm() {
+    return (
+      <div className="custom-control custom-checkbox custom-checkbox-warning">
+        <input
+          className="custom-control-input"
+          id="deleteRecursively"
+          type="checkbox"
+          checked={isDeleteRecursively}
+          onChange={changeIsDeleteRecursivelyHandler}
+        />
+        <label className="custom-control-label" htmlFor="deleteRecursively">
+          { t('modal_delete.delete_recursively') }
+          <p className="form-text text-muted mt-0"><code>{path}</code> { t('modal_delete.recursively') }</p>
+        </label>
+      </div>
+    );
+  }
+
+  function renderDeleteCompletelyForm() {
+    return (
+      <div className="custom-control custom-checkbox custom-checkbox-danger">
+        <input
+          className="custom-control-input"
+          name="completely"
+          id="deleteCompletely"
+          type="checkbox"
+          disabled={!isAbleToDeleteCompletely}
+          checked={isDeleteCompletely}
+          onChange={changeIsDeleteCompletelyHandler}
+        />
+        <label className="custom-control-label text-danger" htmlFor="deleteCompletely">
+          { t('modal_delete.delete_completely') }
+          <p className="form-text text-muted mt-0"> { t('modal_delete.completely') }</p>
+        </label>
+        {!isAbleToDeleteCompletely
+        && (
+        <p className="alert alert-warning p-2 my-0">
+          <i className="icon-ban icon-fw"></i>{ t('modal_delete.delete_completely_restriction') }
+        </p>
+        )}
+      </div>
+    );
+  }
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={onClose} className={`bg-${deleteIconAndKey[deleteMode].color} text-light`}>
+        <i className={`icon-fw icon-${deleteIconAndKey[deleteMode].icon}`}></i>
+        { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{ t('modal_delete.deleting_page') }:</label><br />
+          <code>{ path }</code>
+        </div>
+        {renderDeleteRecursivelyForm()}
+        {!isDeleteCompletelyModal && renderDeleteCompletelyForm()}
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className={`m-l-10 btn btn-${deleteIconAndKey[deleteMode].color}`} onClick={deleteButtonHandler}>
+          <i className={`icon-${deleteIconAndKey[deleteMode].icon}`} aria-hidden="true"></i>
+          { t(`modal_delete.delete_${deleteIconAndKey[deleteMode].translationKey}`) }
+        </button>
+      </ModalFooter>
+    </Modal>
+
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageDeleteModalWrapper = (props) => {
+  return createSubscribedElement(PageDeleteModal, props, [PageContainer]);
+};
+
+PageDeleteModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+
+  path: PropTypes.string.isRequired,
+  isDeleteCompletelyModal: PropTypes.bool,
+  isAbleToDeleteCompletely: PropTypes.bool,
+};
+
+PageDeleteModal.defaultProps = {
+  isDeleteCompletelyModal: false,
+};
+
+export default withTranslation()(PageDeleteModalWrapper);

+ 130 - 0
src/client/js/components/PageDuplicateModal.jsx

@@ -0,0 +1,130 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+import PagePathAutoComplete from './PagePathAutoComplete';
+import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const PageDuplicateModal = (props) => {
+  const { t, appContainer, pageContainer } = props;
+
+  const config = appContainer.getConfig();
+  const isReachable = config.isSearchServiceReachable;
+  const { pageId, path } = pageContainer.state;
+  const { crowi } = appContainer.config;
+
+  const [pageNameInput, setPageNameInput] = useState(path);
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  /**
+   * change pageNameInput for PagePathAutoComplete
+   * @param {string} value
+   */
+  function ppacInputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  /**
+   * change pageNameInput
+   * @param {string} value
+   */
+  function inputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  async function duplicate() {
+    try {
+      setErrorCode(null);
+      setErrorMessage(null);
+      const res = await appContainer.apiPost('/pages.duplicate', { page_id: pageId, new_path: pageNameInput });
+      const { page } = res;
+      window.location.href = encodeURI(`${page.path}?duplicated=${path}`);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  function ppacSubmitHandler() {
+    duplicate();
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-duplicate-page">
+      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
+        { t('modal_duplicate.label.Duplicate page') }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{ t('modal_duplicate.label.Current page name') }</label><br />
+          <code>{ path }</code>
+        </div>
+        <div className="form-group">
+          <label htmlFor="duplicatePageName">{ t('modal_duplicate.label.New page name') }</label><br />
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">{crowi.url}</span>
+            </div>
+            <div className="flex-fill">
+              {isReachable
+              ? (
+                <PagePathAutoComplete
+                  crowi={appContainer}
+                  initializedPath={path}
+                  addTrailingSlash
+                  onSubmit={ppacSubmitHandler}
+                  onInputChange={ppacInputChangeHandler}
+                />
+              )
+              : (
+                <input
+                  type="text"
+                  value={pageNameInput}
+                  className="form-control"
+                  onChange={e => inputChangeHandler(e.target.value)}
+                  required
+                />
+              )}
+            </div>
+          </div>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className="btn btn-primary" onClick={duplicate}>Duplicate page</button>
+      </ModalFooter>
+    </Modal>
+
+  );
+};
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageDuplicateModallWrapper = (props) => {
+  return createSubscribedElement(PageDuplicateModal, props, [AppContainer, PageContainer]);
+};
+
+
+PageDuplicateModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(PageDuplicateModallWrapper);

+ 83 - 0
src/client/js/components/PageManagement/ApiErrorMessage.jsx

@@ -0,0 +1,83 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+const ApiErrorMessage = (props) => {
+  const {
+    t, errorCode, errorMessage, linkPath,
+  } = props;
+
+  function reload() {
+    window.location.reload();
+  }
+
+  function renderMessageByErrorCode() {
+    switch (errorCode) {
+      case 'already_exists':
+        return (
+          <>
+            <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.already_exists') }</strong>
+            <small><a href={linkPath}>{linkPath} <i className="icon-login"></i></a></small>
+          </>
+        );
+      case 'notfound_or_forbidden':
+        return (
+          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.notfound_or_forbidden') }</strong>
+        );
+      case 'user_not_admin':
+        return (
+          <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.user_not_admin') }</strong>
+        );
+      case 'outdated':
+        return (
+          <>
+            <strong><i className="icon-fw icon-bulb"></i> { t('page_api_error.outdated') }</strong>
+            <a className="btn-link" onClick={reload}>
+              <i className="fa fa-angle-double-right"></i> { t('Load latest') }
+            </a>
+          </>
+        );
+      case 'invalid_path':
+        return (
+          <strong><i className="icon-fw icon-ban"></i> Invalid path</strong>
+        );
+      case 'unknown':
+        return (
+          <strong><i className="icon-fw icon-ban"></i> Unknown error occured</strong>
+        );
+      default:
+        return null;
+    }
+  }
+
+  if (errorCode != null) {
+    return (
+      <span className="text-danger">
+        {renderMessageByErrorCode()}
+      </span>
+    );
+  }
+
+  if (errorMessage != null) {
+    return (
+      <span className="text-danger">
+        {errorMessage}
+      </span>
+    );
+  }
+
+  // render null if no error has occurred
+  return null;
+
+};
+
+ApiErrorMessage.propTypes = {
+  t:            PropTypes.func.isRequired, //  i18next
+
+  errorCode:    PropTypes.string,
+  errorMessage: PropTypes.string,
+  linkPath:     PropTypes.string,
+};
+
+export default withTranslation()(ApiErrorMessage);

+ 38 - 40
src/client/js/components/PagePathAutoComplete.jsx

@@ -5,65 +5,63 @@ import { pathUtils } from 'growi-commons';
 
 import SearchTypeahead from './SearchTypeahead';
 
-export default class PagePathAutoComplete extends React.Component {
+const PagePathAutoComplete = (props) => {
 
-  constructor(props) {
+  const {
+    addTrailingSlash, onSubmit, onInputChange, initializedPath,
+  } = props;
 
-    super(props);
+  function inputChangeHandler(pages) {
+    if (onInputChange == null) {
+      return;
+    }
+    const page = pages[0]; // should be single page selected
 
-    this.state = {
-    };
-
-    this.crowi = this.props.crowi;
-
-    this.onSubmit = this.onSubmit.bind(this);
-    this.getKeywordOnInit = this.getKeywordOnInit.bind(this);
-  }
-
-  componentDidMount() {
-  }
-
-  componentWillUnmount() {
+    if (page != null) {
+      onInputChange(page.path);
+    }
   }
 
-  onSubmit(query) {
-    // get the closest form element
-    const elem = this.rootDom;
-    const form = elem.closest('form');
-    // submit with jQuery
-    $(form).submit();
+  function submitHandler() {
+    if (onSubmit == null) {
+      return;
+    }
+    onSubmit();
   }
 
-  getKeywordOnInit(path) {
-    return this.props.addTrailingSlash
+  function getKeywordOnInit(path) {
+    return addTrailingSlash
       ? pathUtils.addTrailingSlash(path)
       : pathUtils.removeTrailingSlash(path);
   }
 
-  render() {
-    return (
-      <div ref={(c) => { this.rootDom = c }}>
-        <SearchTypeahead
-          ref={this.searchTypeaheadDom}
-          crowi={this.crowi}
-          onSubmit={this.onSubmit}
-          inputName="new_path"
-          emptyLabelExceptError={null}
-          placeholder="Input page path"
-          keywordOnInit={this.getKeywordOnInit(this.props.initializedPath)}
-        />
-      </div>
-    );
-  }
+  return (
+    <SearchTypeahead
+      crowi={props.crowi}
+      onSubmit={submitHandler}
+      onChange={inputChangeHandler}
+      onInputChange={props.onInputChange}
+      inputName="new_path"
+      emptyLabelExceptError={null}
+      placeholder="Input page path"
+      keywordOnInit={getKeywordOnInit(initializedPath)}
+    />
+  );
 
-}
+};
 
 PagePathAutoComplete.propTypes = {
   crowi:            PropTypes.object.isRequired,
   initializedPath:  PropTypes.string,
   addTrailingSlash: PropTypes.bool,
+
+  onSubmit:         PropTypes.func,
+  onInputChange:    PropTypes.func,
 };
 
 PagePathAutoComplete.defaultProps = {
   initializedPath: '/',
+  addTrailingSlash: true,
 };
+
+export default PagePathAutoComplete;

+ 172 - 0
src/client/js/components/PageRenameModal.jsx

@@ -0,0 +1,172 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+import AppContainer from '../services/AppContainer';
+import PageContainer from '../services/PageContainer';
+import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const PageRenameModal = (props) => {
+  const {
+    t, appContainer, pageContainer,
+  } = props;
+
+  const { path } = pageContainer.state;
+
+  const { crowi } = appContainer.config;
+
+  const [pageNameInput, setPageNameInput] = useState(path);
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
+  const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
+  const [isRenameMetadata, SetIsRenameMetadata] = useState(false);
+
+  function changeIsRenameRecursivelyHandler() {
+    SetIsRenameRecursively(!isRenameRecursively);
+  }
+
+  function changeIsRenameRedirectHandler() {
+    SetIsRenameRedirect(!isRenameRedirect);
+  }
+
+  function changeIsRenameMetadataHandler() {
+    SetIsRenameMetadata(!isRenameMetadata);
+  }
+
+  /**
+   * change pageNameInput
+   * @param {string} value
+   */
+  function inputChangeHandler(value) {
+    setPageNameInput(value);
+  }
+
+  async function rename() {
+    try {
+      setErrorCode(null);
+      setErrorMessage(null);
+
+      const response = await pageContainer.rename(
+        pageNameInput,
+        isRenameRecursively,
+        isRenameRedirect,
+        isRenameMetadata,
+      );
+      const { page } = response;
+      window.location.href = encodeURI(`${page.path}?renamed=${path}`);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  return (
+    <Modal isOpen={props.isOpen} toggle={props.onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={props.onClose} className="bg-primary text-light">
+        { t('modal_rename.label.Move/Rename page') }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{ t('modal_rename.label.Current page name') }</label><br />
+          <code>{ path }</code>
+        </div>
+        <div className="form-group">
+          <label htmlFor="newPageName">{ t('modal_rename.label.New page name') }</label><br />
+          <div className="input-group">
+            <div className="input-group-prepend">
+              <span className="input-group-text">{crowi.url}</span>
+            </div>
+            <div className="flex-fill">
+              <input
+                type="text"
+                value={pageNameInput}
+                className="form-control"
+                onChange={e => inputChangeHandler(e.target.value)}
+                required
+              />
+            </div>
+          </div>
+        </div>
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
+          <input
+            className="custom-control-input"
+            name="recursively"
+            id="cbRenameRecursively"
+            type="checkbox"
+            checked={isRenameRecursively}
+            onChange={changeIsRenameRecursivelyHandler}
+          />
+          <label className="custom-control-label" htmlFor="cbRenameRecursively">
+            { t('modal_rename.label.Recursively') }
+            <p className="form-text text-muted mt-0">{ t('modal_rename.help.recursive') }</p>
+          </label>
+        </div>
+
+        <div className="custom-control custom-checkbox custom-checkbox-success">
+          <input
+            className="custom-control-input"
+            name="create_redirect"
+            id="cbRenameRedirect"
+            type="checkbox"
+            checked={isRenameRedirect}
+            onChange={changeIsRenameRedirectHandler}
+          />
+          <label className="custom-control-label" htmlFor="cbRenameRedirect">
+            { t('modal_rename.label.Redirect') }
+            <p className="form-text text-muted mt-0">{ t('modal_rename.help.redirect') }</p>
+          </label>
+        </div>
+
+        <div className="custom-control custom-checkbox custom-checkbox-primary">
+          <input
+            className="custom-control-input"
+            name="remain_metadata"
+            id="cbRenameMetadata"
+            type="checkbox"
+            checked={isRenameMetadata}
+            onChange={changeIsRenameMetadataHandler}
+          />
+          <label className="custom-control-label" htmlFor="cbRenameMetadata">
+            { t('modal_rename.label.Do not update metadata') }
+            <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
+          </label>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className="btn btn-primary" onClick={rename}>Rename</button>
+      </ModalFooter>
+    </Modal>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageRenameModalWrapper = (props) => {
+  return createSubscribedElement(PageRenameModal, props, [AppContainer, PageContainer]);
+};
+
+
+PageRenameModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+
+  path: PropTypes.string.isRequired,
+};
+
+export default withTranslation()(PageRenameModalWrapper);

+ 104 - 0
src/client/js/components/PutbackPageModal.jsx

@@ -0,0 +1,104 @@
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import {
+  Modal, ModalHeader, ModalBody, ModalFooter,
+} from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from './UnstatedUtils';
+
+import PageContainer from '../services/PageContainer';
+
+import ApiErrorMessage from './PageManagement/ApiErrorMessage';
+
+const PutBackPageModal = (props) => {
+  const {
+    t, isOpen, onClose, pageContainer, path,
+  } = props;
+
+  const [errorCode, setErrorCode] = useState(null);
+  const [errorMessage, setErrorMessage] = useState(null);
+
+  const [isPutbackRecursively, setIsPutbackRecursively] = useState(true);
+
+  function changeIsPutbackRecursivelyHandler() {
+    setIsPutbackRecursively(!isPutbackRecursively);
+  }
+
+  async function putbackPage() {
+    setErrorCode(null);
+    setErrorMessage(null);
+
+    try {
+      const response = await pageContainer.revertRemove(isPutbackRecursively);
+      const putbackPagePath = response.page.path;
+      window.location.href = encodeURI(putbackPagePath);
+    }
+    catch (err) {
+      setErrorCode(err.code);
+      setErrorMessage(err.message);
+    }
+  }
+
+  async function putbackPageButtonHandler() {
+    putbackPage();
+  }
+
+  return (
+    <Modal isOpen={isOpen} toggle={onClose} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={onClose} className="bg-info text-light">
+        <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('modal_putback.label.Put Back Page') }
+      </ModalHeader>
+      <ModalBody>
+        <div className="form-group">
+          <label>{t('modal_putback.label.Put Back Page')}:</label><br />
+          <code>{path}</code>
+        </div>
+        <div className="custom-control custom-checkbox custom-checkbox-warning">
+          <input
+            className="custom-control-input"
+            id="cbPutBackRecursively"
+            type="checkbox"
+            checked={isPutbackRecursively}
+            onChange={changeIsPutbackRecursivelyHandler}
+          />
+          <label htmlFor="cbPutBackRecursively" className="custom-control-label">
+            { t('modal_putback.label.recursively') }
+          </label>
+          <p className="form-text text-muted mt-0">
+            <code>{ path }</code>{ t('modal_putback.help.recursively') }
+          </p>
+        </div>
+      </ModalBody>
+      <ModalFooter>
+        <ApiErrorMessage errorCode={errorCode} errorMessage={errorMessage} linkPath={path} />
+        <button type="button" className="btn btn-info" onClick={putbackPageButtonHandler}>
+          <i className="icon-action-undo mr-2" aria-hidden="true"></i> { t('Put Back') }
+        </button>
+      </ModalFooter>
+    </Modal>
+  );
+
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PutBackPageModalWrapper = (props) => {
+  return createSubscribedElement(PutBackPageModal, props, [PageContainer]);
+};
+
+PutBackPageModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+
+  isOpen: PropTypes.bool.isRequired,
+  onClose: PropTypes.func.isRequired,
+
+  path: PropTypes.string.isRequired,
+};
+
+
+export default withTranslation()(PutBackPageModalWrapper);

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

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

+ 25 - 37
src/client/js/components/Sidebar.jsx

@@ -38,44 +38,32 @@ class Sidebar extends React.Component {
   initBreakpointEvents() {
     const { appContainer, navigationUIController } = this.props;
 
-    document.addEventListener('DOMContentLoaded', () => {
-      // get the value of '--breakpoint-*'
-      // const breakpointSm = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-sm'), 10);
-      const breakpointMd = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue('--breakpoint-md'), 10);
-
-      const smHandler = (mql) => {
-        if (mql.matches) {
-          // cache width
-          this.sidebarWidthCached = navigationUIController.state.productNavWidth;
-
-          appContainer.setState({ isDrawerOpened: false });
-          navigationUIController.disableResize();
-          navigationUIController.expand();
-
-          // fix width
-          navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+    const mdOrAvobeHandler = (mql) => {
+      // sm -> md
+      if (mql.matches) {
+        appContainer.setState({ isDrawerOpened: false });
+        navigationUIController.enableResize();
+
+        // restore width
+        if (this.sidebarWidthCached != null) {
+          navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
         }
-        else {
-          appContainer.setState({ isDrawerOpened: false });
-          navigationUIController.enableResize();
-
-          // restore width
-          if (this.sidebarWidthCached != null) {
-            navigationUIController.setState({ productNavWidth: this.sidebarWidthCached });
-          }
-        }
-      };
-
-      // const mediaQueryForXs = window.matchMedia(`(max-width: ${breakpointSm}px)`);
-      const mediaQueryForSm = window.matchMedia(`(max-width: ${breakpointMd}px)`);
-
-      // add event listener
-      // mediaQueryForXs.addListener(xsHandler);
-      mediaQueryForSm.addListener(smHandler);
-      // initialize
-      // xsHandler(mediaQueryForXs);
-      smHandler(mediaQueryForSm);
-    });
+      }
+      // md -> sm
+      else {
+        // cache width
+        this.sidebarWidthCached = navigationUIController.state.productNavWidth;
+
+        appContainer.setState({ isDrawerOpened: false });
+        navigationUIController.disableResize();
+        navigationUIController.expand();
+
+        // fix width
+        navigationUIController.setState({ productNavWidth: sidebarDefaultWidth });
+      }
+    };
+
+    appContainer.addBreakpointListener('md', mdOrAvobeHandler, true);
   }
 
   backdropClickedHandler = () => {

+ 0 - 24
src/client/js/installer.jsx

@@ -1,24 +0,0 @@
-import React from 'react';
-import ReactDOM from 'react-dom';
-import { I18nextProvider } from 'react-i18next';
-
-import i18nFactory from './util/i18n';
-
-import InstallerForm from './components/InstallerForm';
-
-const i18n = i18nFactory();
-
-// render InstallerForm
-const installerFormElem = document.getElementById('installer-form');
-if (installerFormElem) {
-  const userName = installerFormElem.dataset.userName;
-  const name = installerFormElem.dataset.name;
-  const email = installerFormElem.dataset.email;
-  const csrf = installerFormElem.dataset.csrf;
-  ReactDOM.render(
-    <I18nextProvider i18n={i18n}>
-      <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
-    </I18nextProvider>,
-    installerFormElem,
-  );
-}

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

@@ -1,7 +1,4 @@
 /* eslint-disable react/jsx-filename-extension */
-
-import { pathUtils } from 'growi-commons';
-
 require('jquery.cookie');
 
 require('./thirdparty-js/waves');
@@ -182,7 +179,6 @@ Crowi.highlightSelectedSection = function(hash) {
 
 $(() => {
   const appContainer = window.appContainer;
-  const websocketContainer = appContainer.getContainer('WebsocketContainer');
   const config = appContainer.getConfig();
 
   const pageId = $('#content-main').data('page-id');
@@ -190,7 +186,6 @@ $(() => {
   // const revisionCreatedAt = $('#content-main').data('page-revision-created');
   // const currentUser = $('#content-main').data('current-user');
   const isSeen = $('#content-main').data('page-is-seen');
-  const pagePath = $('#content-main').data('path');
   const isSavedStatesOfTabChanges = config.isSavedStatesOfTabChanges;
 
   $('[data-toggle="popover"]').popover();
@@ -218,258 +213,7 @@ $(() => {
     $(this).select();
   });
 
-
-  // TODO GW-2355 remove this after refactoring
-  $('#create-page').on('shown.bs.modal', (e) => {
-    // quick hack: replace from server side rendering "date" to client side "date"
-    const today = new Date();
-    const month = (`0${today.getMonth() + 1}`).slice(-2);
-    const day = (`0${today.getDate()}`).slice(-2);
-    const dateString = `${today.getFullYear()}/${month}/${day}`;
-    $('#create-page-today .page-today-suffix').text(`/${dateString}/`);
-    $('#create-page-today .page-today-input2').data('prefix', `/${dateString}/`);
-
-    // focus
-    $('#create-page-today .page-today-input2').eq(0).focus();
-  });
-
-  $('#create-page-today').submit(function(e) {
-    let prefix1 = $('input.page-today-input1', this).data('prefix');
-    let prefix2 = $('input.page-today-input2', this).data('prefix');
-    const input1 = $('input.page-today-input1', this).val();
-    const input2 = $('input.page-today-input2', this).val();
-    if (input1 === '') {
-      prefix1 = 'メモ';
-    }
-    if (input2 === '') {
-      prefix2 = prefix2.slice(0, -1);
-    }
-    window.location.href = `${prefix1 + input1 + prefix2 + input2}#edit`;
-    return false;
-  });
-
-  $('#create-page-under-tree').submit(function(e) {
-    let name = $('input', this).val();
-    if (!name.match(/^\//)) {
-      name = `/${name}`;
-    }
-    if (name.match(/.+\/$/)) {
-      name = name.substr(0, name.length - 1);
-    }
-    // TODO: remove by GW-2278
-    window.location.href = `${pathUtils.encodePagePath(name)}#edit`;
-    return false;
-  });
-
-  // rename
-  $('#renamePage').on('shown.bs.modal', (e) => {
-    $('#renamePage #newPageName').focus();
-    $('#renamePage .msg').hide();
-  });
-  $('#renamePageForm').submit(function(e) {
-    // create name-value map
-    const nameValueMap = {};
-    $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is renamed page path
-    });
-    nameValueMap.socketClientId = websocketContainer.getSocketClientId();
-
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.rename',
-      data: nameValueMap,
-      dataType: 'json',
-    })
-      .done((res) => {
-      // error
-        if (!res.ok) {
-          const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
-          $('#renamePage .msg').hide();
-          $(`#renamePage .msg-${res.code}`).show();
-          $('#renamePage #linkToNewPage').html(`
-          <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
-        `);
-        }
-        else {
-          const page = res.page;
-          window.location.href = `${page.path}?renamed=${pagePath}`;
-        }
-      });
-
-    return false;
-  });
-
-  // duplicate
-  $('#duplicatePage').on('shown.bs.modal', (e) => {
-    $('#duplicatePage #duplicatePageName').focus();
-    $('#duplicatePage .msg').hide();
-  });
-  $('#duplicatePageForm').submit(function(e) {
-    // create name-value map
-    const nameValueMap = {};
-    $(this).serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value; // nameValueMap.new_path is duplicated page path
-    });
-    nameValueMap.socketClientId = websocketContainer.getSocketClientId();
-
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.duplicate',
-      data: nameValueMap,
-      dataType: 'json',
-    }).done((res) => {
-      // error
-      if (!res.ok) {
-        const linkPath = pathUtils.normalizePath(nameValueMap.new_path);
-        $('#duplicatePage .msg').hide();
-        $(`#duplicatePage .msg-${res.code}`).show();
-        $('#duplicatePage #linkToNewPage').html(`
-          <a href="${linkPath}">${linkPath} <i class="icon-login"></i></a>
-        `);
-      }
-      else {
-        const page = res.page;
-        window.location.href = `${page.path}?duplicated=${pagePath}`;
-      }
-    });
-
-    return false;
-  });
-
-  // empty trash
-  $('#emptyTrash').on('shown.bs.modal', (e) => {
-    $('#emptyTrash .msg').hide();
-  });
-  $('#empty-trash-form').submit((e) => {
-    // create name-value map
-    const nameValueMap = {};
-    $('#empty-trash-form').serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value;
-    });
-    $.ajax({
-      type: 'DELETE',
-      url: '/_api/v3/pages/empty-trash',
-      data: nameValueMap,
-      dataType: 'json',
-    }).done((res) => {
-      window.location.href = '/trash';
-    }).fail((jqXHR, textStatus, errorThrown) => {
-      $('#emptyTrash .msg').hide();
-      $('#emptyTrash .msg-unknown').show();
-    });
-
-    return false;
-  });
-  // delete
-  $('#deletePage').on('shown.bs.modal', (e) => {
-    $('#deletePage .msg').hide();
-  });
-  $('#delete-page-form').submit((e) => {
-    // create name-value map
-    const nameValueMap = {};
-    $('#delete-page-form').serializeArray().forEach((obj) => {
-      nameValueMap[obj.name] = obj.value;
-    });
-    nameValueMap.socketClientId = websocketContainer.getSocketClientId();
-
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.remove',
-      data: nameValueMap,
-      dataType: 'json',
-    }).done((res) => {
-      // error
-      if (!res.ok) {
-        $('#deletePage .msg').hide();
-        $(`#deletePage .msg-${res.code}`).show();
-      }
-      else {
-        const page = res.page;
-        window.location.href = page.path;
-      }
-    });
-
-    return false;
-  });
-
-  // Put Back
-  $('#putBackPage').on('shown.bs.modal', (e) => {
-    $('#putBackPage .msg').hide();
-  });
-  $('#revert-delete-page-form').submit((e) => {
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.revertRemove',
-      data: $('#revert-delete-page-form').serialize(),
-      dataType: 'json',
-    }).done((res) => {
-      // error
-      if (!res.ok) {
-        $('#putBackPage .msg').hide();
-        $(`#putBackPage .msg-${res.code}`).show();
-      }
-      else {
-        const page = res.page;
-        window.location.href = page.path;
-      }
-    });
-
-    return false;
-  });
-  $('#unlink-page-form').submit((e) => {
-    $.ajax({
-      type: 'POST',
-      url: '/_api/pages.unlink',
-      data: $('#unlink-page-form').serialize(),
-      dataType: 'json',
-    })
-      .done((res) => {
-        if (!res.ok) {
-          $('#delete-errors').html(`<i class="fa fa-times-circle"></i> ${res.error}`);
-          $('#delete-errors').addClass('alert-danger');
-        }
-        else {
-          window.location.href = `${res.path}?unlinked=true`;
-        }
-      });
-
-    return false;
-  });
-
-  $('#create-portal-button').on('click', (e) => {
-    $('a[data-toggle="tab"][href="#edit"]').tab('show');
-
-    $('body').addClass('on-edit');
-    $('body').addClass('builtin-editor');
-
-    const path = $('.content-main').data('path');
-    if (path !== '/' && $('.content-main').data('page-id') === '') {
-      const upperPage = path.substr(0, path.length - 1);
-      $.get('/_api/pages.get', { path: upperPage }, (res) => {
-        if (res.ok && res.page) {
-          $('#portal-warning-modal').modal('show');
-        }
-      });
-    }
-  });
-  $('#portal-form-close').on('click', (e) => {
-    $('#edit').removeClass('active');
-    $('body').removeClass('on-edit');
-    $('body').removeClass('builtin-editor');
-    window.location.hash = '#';
-  });
-
   if (pageId) {
-    // for Crowi Template LangProcessor
-    $('.template-create-button', $('#revision-body')).on('click', function() {
-      const path = $(this).data('path');
-      const templateId = $(this).data('template');
-      const template = $(`#${templateId}`).html();
-
-      const editorContainer = appContainer.getContainer('EditorContainer');
-      editorContainer.saveDraft(path, template);
-      window.location.href = `${path}#edit`;
-    });
 
     if (!isSeen) {
       $.post('/_api/pages.seen', { page_id: pageId }, (res) => {

+ 81 - 0
src/client/js/nologin.jsx

@@ -0,0 +1,81 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { Provider } from 'unstated';
+import { I18nextProvider } from 'react-i18next';
+
+import i18nFactory from './util/i18n';
+
+import NoLoginContainer from './services/NoLoginContainer';
+import AppContainer from './services/AppContainer';
+
+import InstallerForm from './components/InstallerForm';
+import LoginForm from './components/LoginForm';
+
+const i18n = i18nFactory();
+
+// render InstallerForm
+const installerFormElem = document.getElementById('installer-form');
+if (installerFormElem) {
+  const userName = installerFormElem.dataset.userName;
+  const name = installerFormElem.dataset.name;
+  const email = installerFormElem.dataset.email;
+  const csrf = installerFormElem.dataset.csrf;
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <InstallerForm userName={userName} name={name} email={email} csrf={csrf} />
+    </I18nextProvider>,
+    installerFormElem,
+  );
+}
+
+// render loginForm
+const loginFormElem = document.getElementById('login-form');
+if (loginFormElem) {
+  const noLoginContainer = new NoLoginContainer();
+  const appContainer = new AppContainer();
+  appContainer.init();
+
+  const username = loginFormElem.dataset.username;
+  const name = loginFormElem.dataset.name;
+  const email = loginFormElem.dataset.email;
+  const isRegistrationEnabled = loginFormElem.dataset.isRegistrationEnabled === 'true';
+  const registrationMode = loginFormElem.dataset.registrationMode;
+
+
+  let registrationWhiteList = loginFormElem.dataset.registrationWhiteList;
+  registrationWhiteList = registrationWhiteList.length > 0
+    ? registrationWhiteList = loginFormElem.dataset.registrationWhiteList.split(',')
+    : registrationWhiteList = [];
+
+
+  const isLocalStrategySetup = loginFormElem.dataset.isLocalStrategySetup === 'true';
+  const isLdapStrategySetup = loginFormElem.dataset.isLdapStrategySetup === 'true';
+  const objOfIsExternalAuthEnableds = {
+    google: loginFormElem.dataset.isGoogleAuthEnabled === 'true',
+    github: loginFormElem.dataset.isGithubAuthEnabled === 'true',
+    facebook: loginFormElem.dataset.isFacebookAuthEnabled === 'true',
+    twitter: loginFormElem.dataset.isTwitterAuthEnabled === 'true',
+    saml: loginFormElem.dataset.isSamlAuthEnabled === 'true',
+    oidc: loginFormElem.dataset.isOidcAuthEnabled === 'true',
+    basic: loginFormElem.dataset.isBasicAuthEnabled === 'true',
+  };
+
+  ReactDOM.render(
+    <I18nextProvider i18n={i18n}>
+      <Provider inject={[noLoginContainer, appContainer]}>
+        <LoginForm
+          username={username}
+          name={name}
+          email={email}
+          isRegistrationEnabled={isRegistrationEnabled}
+          registrationMode={registrationMode}
+          registrationWhiteList={registrationWhiteList}
+          isLocalStrategySetup={isLocalStrategySetup}
+          isLdapStrategySetup={isLdapStrategySetup}
+          objOfIsExternalAuthEnableds={objOfIsExternalAuthEnableds}
+        />
+      </Provider>
+    </I18nextProvider>,
+    loginFormElem,
+  );
+}

+ 32 - 1
src/client/js/services/AppContainer.js

@@ -8,6 +8,8 @@ import InterceptorManager from '@commons/service/interceptor-manager';
 import emojiStrategy from '../util/emojione/emoji_strategy_shrinked.json';
 import GrowiRenderer from '../util/GrowiRenderer';
 
+import Apiv1ErrorHandler from '../util/apiv1ErrorHandler';
+
 import {
   DetachCodeBlockInterceptor,
   RestoreCodeBlockInterceptor,
@@ -33,6 +35,7 @@ export default class AppContainer extends Container {
       editorMode: null,
       preferDarkModeByMediaQuery: false,
       preferDarkModeByUser: null,
+      breakpoint: 'xs',
       isDrawerOpened: false,
 
       isPageCreateModalShown: false,
@@ -108,7 +111,6 @@ export default class AppContainer extends Container {
   }
 
   init() {
-    // this.initBreakpointEvents();
     this.initColorScheme();
     this.initPlugins();
   }
@@ -233,6 +235,28 @@ export default class AppContainer extends Container {
     return this.componentInstances[id];
   }
 
+  /**
+   *
+   * @param {string} breakpoint id of breakpoint
+   * @param {function} handler event handler for media query
+   * @param {boolean} invokeOnInit invoke handler after the initialization if true
+   */
+  addBreakpointListener(breakpoint, handler, invokeOnInit = false) {
+    document.addEventListener('DOMContentLoaded', () => {
+      // get the value of '--breakpoint-*'
+      const breakpointPixel = parseInt(window.getComputedStyle(document.documentElement).getPropertyValue(`--breakpoint-${breakpoint}`), 10);
+
+      const mediaQuery = window.matchMedia(`(min-width: ${breakpointPixel}px)`);
+
+      // add event listener
+      mediaQuery.addListener(handler);
+      // initialize
+      if (invokeOnInit) {
+        handler(mediaQuery);
+      }
+    });
+  }
+
   getOriginRenderer() {
     return this.originRenderer;
   }
@@ -428,6 +452,13 @@ export default class AppContainer extends Container {
     if (res.data.ok) {
       return res.data;
     }
+
+    // Return error code if code is exist
+    if (res.data.code != null) {
+      const error = new Apiv1ErrorHandler(res.data.error, res.data.code);
+      throw error;
+    }
+
     throw new Error(res.data.error);
   }
 

+ 23 - 0
src/client/js/services/NoLoginContainer.js

@@ -0,0 +1,23 @@
+import { Container } from 'unstated';
+
+/**
+ * Service container related to Nologin (installer, login)
+ * @extends {Container} unstated Container
+ */
+export default class NoLoginContainer extends Container {
+
+  constructor() {
+    super();
+
+    const body = document.querySelector('body');
+    this.csrfToken = body.dataset.csrftoken;
+  }
+
+  /**
+   * Workaround for the mangling in production build to break constructor.name
+   */
+  static getClassName() {
+    return 'NoLoginContainer';
+  }
+
+}

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

@@ -4,6 +4,7 @@ import loggerFactory from '@alias/logger';
 
 import * as entities from 'entities';
 import * as toastr from 'toastr';
+import { toastError } from '../util/apiNotification';
 
 const logger = loggerFactory('growi:services:PageContainer');
 const scrollThresForSticky = 0;
@@ -32,7 +33,6 @@ export default class PageContainer extends Container {
 
     const revisionId = mainContent.getAttribute('data-page-revision-id');
     const path = decodeURI(mainContent.getAttribute('data-path'));
-
     this.state = {
       // local page data
       markdown: null, // will be initialized after initStateMarkdown()
@@ -48,8 +48,11 @@ export default class PageContainer extends Container {
       createdAt: mainContent.getAttribute('data-page-created-at'),
       creator: JSON.parse(mainContent.getAttribute('data-page-creator')),
       updatedAt: mainContent.getAttribute('data-page-updated-at'),
-
+      isDeleted:  JSON.parse(mainContent.getAttribute('data-page-is-deleted')),
+      isDeletable:  JSON.parse(mainContent.getAttribute('data-page-is-deletable')),
+      isAbleToDeleteCompletely:  JSON.parse(mainContent.getAttribute('data-page-is-able-to-delete-completely')),
       tags: [],
+      hasChildren: JSON.parse(mainContent.getAttribute('data-page-has-children')),
       templateTagData: mainContent.getAttribute('data-template-tags') || null,
 
       // latest(on remote) information
@@ -85,6 +88,20 @@ export default class PageContainer extends Container {
         isSubnavCompact: scrollThresForCompact < currentYOffset,
       });
     });
+
+    const unlinkPageButton = document.getElementById('unlink-page-button');
+    if (unlinkPageButton != null) {
+      unlinkPageButton.addEventListener('click', async() => {
+        try {
+          const res = await this.appContainer.apiPost('/pages.unlink', { path });
+          window.location.href = encodeURI(`${res.path}?unlinked=true`);
+        }
+        catch (err) {
+          toastError(err);
+        }
+      });
+    }
+
   }
 
   /**
@@ -294,6 +311,53 @@ export default class PageContainer extends Container {
     return { page: res.page, tags: res.tags };
   }
 
+  deletePage(isRecursively, isCompletely) {
+    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+
+    // control flag
+    const completely = isCompletely ? true : null;
+    const recursively = isRecursively ? true : null;
+
+    return this.appContainer.apiPost('/pages.remove', {
+      recursively,
+      completely,
+      page_id: this.state.pageId,
+      revision_id: this.state.revisionId,
+      socketClientId: websocketContainer.getSocketClientId(),
+    });
+
+  }
+
+  revertRemove(isRecursively) {
+    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+
+    // control flag
+    const recursively = isRecursively ? true : null;
+
+    return this.appContainer.apiPost('/pages.revertRemove', {
+      recursively,
+      page_id: this.state.pageId,
+      socketClientId: websocketContainer.getSocketClientId(),
+    });
+  }
+
+  rename(pageNameInput, isRenameRecursively, isRenameRedirect, isRenameMetadata) {
+    const websocketContainer = this.appContainer.getContainer('WebsocketContainer');
+    const isRecursively = isRenameRecursively ? true : null;
+    const isRedirect = isRenameRedirect ? true : null;
+    const isRemain = isRenameMetadata ? true : null;
+
+    return this.appContainer.apiPost('/pages.rename', {
+      recursively: isRecursively,
+      page_id: this.state.pageId,
+      revision_id: this.state.revisionId,
+      new_path: pageNameInput,
+      create_redirect: isRedirect,
+      remain_metadata: isRemain,
+      socketClientId: websocketContainer.getSocketClientId(),
+    });
+  }
+
   showSuccessToastr() {
     toastr.success(undefined, 'Saved successfully', {
       closeButton: true,

+ 12 - 0
src/client/js/util/apiv1ErrorHandler.js

@@ -0,0 +1,12 @@
+class Apiv1ErrorHandler extends Error {
+
+  constructor(message = '', code = '') {
+    super();
+
+    this.message = message;
+    this.code = code;
+  }
+
+}
+
+module.exports = Apiv1ErrorHandler;

+ 0 - 78
src/client/styles/agile-admin/inverse/variables.scss

@@ -1,78 +0,0 @@
-// Variables
-
-// @import 'https://fonts.googleapis.com/css?family=Rubik:300,400,500,700,900';
-
-// $basefont1:'Rubik', sans-serif;
-// $basefont2:'Rubik', sans-serif;
-// $basefont1: Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif !default;
-// $basefont2: Lato, -apple-system, BlinkMacSystemFont, 'Hiragino Kaku Gothic ProN', Meiryo, sans-serif !default;
-
-/* GROWI Color */
-// $growi-green: #74bc46;
-// $growi-blue: #175fa5;
-
-/*bootstrap Color*/
-// $danger: #ff0a54 !default;
-// $success: #00bb83 !default;
-// $warning: #ffa32b !default;
-// $primary: $growi-blue !default;
-// $info: #009fbb !default;
-// $muted: #98a6ad !default;
-// $dark: #3e4d6c !default;
-// $inverse: #3e4d6c !default;
-// $light: #e4e7ea !default;
-// $extralight: #f7fafc !default;
-
-/*Normal Color*/
-// $white: #ffffff !default;
-// $red: #ff0000 !default;
-// $purple: #7b00ce !default;
-// $blue: #0d00c5 !default;
-// $yellow: #cccf0e !default;
-// $border: #f0f0f0 !default;
-// $megna: #00b5c2 !default;
-
-/*Theme Colors*/
-// $topbar: #3c4451 !default;
-// $sidebar: #4f5467 !default;
-// $bodycolor: #fff !default;
-// $headingtext: #2b2b2b !default;
-// $bodytext: #686868 !default;
-// $linktext: $inverse !default;
-// $linktext-hover: lighten($inverse, 20%) !default;
-// $sidebar-text: #54667a !default;
-// $themecolor: #ff6849 !default;
-// $dark-themecolor: #4f5467 !default;
-
-// $rgt: right !default;
-// $lft: left !default;
-
-// $dark-text: #848a96 !default;
-// $navbar-border: #ccc !default;
-// $active-navbar-border: lighten($navbar-border, 10%) !default;
-// $btn-default-bgcolor: darken(#fff, 10%) !default;
-// $color-inline-code: #c7254e !default;
-// $bgcolor-inline-code: #f9f2f4 !default;
-
-/*Border radius*/
-// $radius: 0 !default;
-
-/*Preloader*/
-/*
-.preloader{
-    width: 100%;
-    height: 100%;
-    top:0px;
-    position: fixed;
-    z-index: 99999;
-    background: #fff;
-    .cssload-speeding-wheel{
-        position: absolute;
-        top: "calc(50% - 3.5px)";
-        left: "calc(50% - 3.5px)";
-    }
-}
-*/
-
-/*Font weight*/
-// $font-bold: 700 !default;

+ 1 - 5
src/client/styles/scss/_admin.scss

@@ -1,8 +1,4 @@
 .admin-page {
-  header.grw-header {
-    height: unset;
-  }
-
   .admin-user-menu {
     .dropdown-menu {
       right: 0;
@@ -168,4 +164,4 @@
 
 .admin-navigation > a + a {
   margin-top: 2px;
-}
+}

+ 14 - 60
src/client/styles/scss/_create-page.scss

@@ -1,61 +1,15 @@
-.modal.create-page {
-  .modal-body {
-    //TODO remove legend
-    legend {
-      margin-bottom: 10px;
-    }
-
-    form,
-    #template-form {
-      // layout
-      .create-page-input-container {
-        .create-page-input-row {
-          flex: 1;
-        }
-        .create-page-button-container {
-          margin-left: 15px;
-          .btn {
-            min-width: 105px;
-          }
-        }
-
-        // change layout by screen size
-        @include media-breakpoint-down(md) {
-          flex-direction: column;
-          .create-page-button-container {
-            margin-top: 10px;
-            text-align: right;
-          }
-        }
-      }
-
-      .page-today-prefix {
-      }
-      .page-today-input1 {
-        width: 60px;
-        padding-right: 2px;
-        padding-left: 2px;
-        margin-right: 5px;
-        margin-left: 5px;
-      }
-      .page-today-suffix {
-      }
-      .page-today-input2 {
-        flex: 1;
-        margin-left: 5px;
-      }
-
-      .page-name-input {
-        flex: 1;
-        input {
-          min-width: 300px; // Workaround to display placeholder.
-          //   cf https://github.com/ericgio/react-bootstrap-typeahead/issues/256
-        }
-      }
-
-      .create-page-under-tree-label code {
-        font-family: $font-family-monospace-not-strictly;
-      }
-    }
-  } // .modal-body
+.grw-create-page {
+  .page-today-input1 {
+    width: 60px;
+  }
+  .page-today-input2 {
+  }
+
+  .grw-btn-create-page {
+    min-width: 90px;
+  }
+
+  .create-page-under-tree-label code {
+    font-family: $font-family-monospace-not-strictly;
+  }
 }

+ 2 - 1
src/client/styles/scss/_editor-attachment.scss

@@ -108,7 +108,8 @@
     border: none;
     border-top: 1px dotted #ccc;
     border-bottom: none;
-    border-radius: 0;
+    // TODO: remove or set value for GW-2531
+    // border-radius: 0;
 
     &:active {
       box-shadow: none;

+ 2 - 1
src/client/styles/scss/_hljs.scss

@@ -2,7 +2,8 @@ pre.hljs {
   position: relative;
 
   // override Highlight Js Style Border
-  border-radius: 3px;
+  border: 1px solid $gray-500;
+  border-radius: $border-radius;
   &.hljs-no-border {
     border: none;
   }

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

@@ -47,7 +47,7 @@
   text-align: center;
   border: solid 1px #ccc;
   border-right: none;
-  border-radius: 5px 0 0 5px;
+  // border-radius: 5px 0 0 5px;
   transition: 0.3s ease;
 
   &:hover {
@@ -114,7 +114,7 @@
       margin-bottom: 20px;
       font-size: 0.9em;
       border: solid 1px #aaa;
-      border-radius: 5px;
+      // border-radius: 5px;
 
       .revision-toc-head {
         display: inline-block;

+ 57 - 189
src/client/styles/scss/_login.scss

@@ -19,6 +19,7 @@
       display: flex;
       align-items: center;
       height: 100vh;
+      margin-top: 0px;
 
       .main {
         width: 100vw;
@@ -106,139 +107,57 @@
     }
   }
 
-  .external-auth {
-    form {
-      flex: 1;
-
-      @media (min-width: 350px) {
-        flex: 0.49;
-      }
-    }
-
-    .spacer {
-      height: 10px;
-    }
-  }
-
   .collapse-external-auth {
     overflow: hidden;
-
-    &:not(.show) {
-      height: 0;
-      padding: 0 !important;
-    }
   }
 
-  // button style
-  .btn-fill.login {
-    .btn-label {
-      background-color: rgba($danger, 0.4);
-    }
-    .eff {
-      background-color: rgba(#7e4153, 0.5);
-    }
-  }
-
-  // google
-  .btn-fill#google {
-    .btn-label {
-      background-color: rgba(#24292e, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // github
-  .btn-fill#github {
-    .btn-label {
-      background-color: rgba(lighten(black, 20%), 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // facebook
-  .btn-fill#facebook {
-    .btn-label {
-      background-color: rgba(#29487d, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // twitter
-  .btn-fill#twitter {
-    .btn-label {
-      background-color: rgba(#1da1f2, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // oidc
-  .btn-fill#oidc {
-    .btn-label {
-      background-color: rgba(#24292e, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // saml
-  .btn-fill#saml {
-    .btn-label {
-      background-color: rgba(#55a79a, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-
-  // basic
-  .btn-fill#basic {
-    .btn-label {
-      background-color: rgba(#24292e, 0.4);
-    }
-
-    .eff {
-      background-color: #555;
-    }
-  }
-  // register
-  .btn-fill#register {
-    .btn-label {
-      background-color: rgba($success, 0.4);
-    }
-
-    .eff {
-      background-color: rgba(#3f7263, 0.5);
-    }
-  }
-
-  // external-auth
-  .btn-collapse-external-auth {
-    color: white;
-    background-color: rgba(lighten(black, 20%), 0.4);
-    border: none;
-
-    .btn-label {
-      padding: 9px 15px;
-      margin: -8px 20px -8px -20px;
-    }
-
-    &:focus {
-      border: none;
+  $btn-fill-colors: (
+    'login': (
+      rgba($danger, 0.4),
+      rgba(#7e4153, 0.5),
+    ),
+    'register': (
+      rgba($success, 0.4),
+      rgba(#3f7263, 0.5),
+    ),
+    'google': (
+      rgba(#24292e, 0.4),
+      #555,
+    ),
+    'github': (
+      rgba(lighten(black, 20%), 0.4),
+      #555,
+    ),
+    'facebook': (
+      rgba(#29487d, 0.4),
+      #555,
+    ),
+    'twitter': (
+      rgba(#1da1f2, 0.4),
+      #555,
+    ),
+    'oidc': (
+      rgba(#24292e, 0.4),
+      #555,
+    ),
+    'saml': (
+      rgba(#55a79a, 0.4),
+      #555,
+    ),
+    'basic': (
+      rgba(#24292e, 0.4),
+      #555,
+    ),
+  );
+
+  @each $label, $colors in $btn-fill-colors {
+    .btn-fill##{$label} {
+      .btn-label {
+        background-color: nth($colors, 1);
+      }
+      .eff {
+        background-color: nth($colors, 2);
+      }
     }
   }
 
@@ -283,16 +202,9 @@
 
 .login-page {
   // layout
-  .main .row {
-    @media (min-width: 350px) {
-      .col-sm-offset-4 {
-        margin-left: calc(50% - 160px);
-      }
-
-      .col-sm-4 {
-        width: 320px;
-      }
-    }
+  .main .row .login-header,
+  .login-dialog {
+    width: 320px;
   }
 
   .link-growi-org {
@@ -301,55 +213,11 @@
     z-index: 2;
   }
 
-  // flip animation
-  .login-dialog.flipper {
-    transition: min-height 0.2s;
-
-    &.to-flip {
-      min-height: 295px;
-
-      // has-error
-      &.has-error {
-        min-height: #{295px + 32px};
-      }
-    }
-
-    .front,
-    .back {
-      transition: 0.4s;
-      backface-visibility: hidden;
-      transform-style: preserve-3d;
-      // fix https://github.com/weseek/growi/issues/330
-      -webkit-backface-visibility: hidden;
-      -webkit-transform-style: preserve-3d;
-    }
-
-    .front {
-      z-index: 2;
-    }
-
-    .back {
-      position: absolute;
-      top: 0;
-      right: 15px;
-      left: 15px;
-    }
-
-    .back,
-    &.to-flip .front {
-      transform: rotateY(180deg);
-
-      // fix https://github.com/weseek/growi/issues/330
-      // 'backface-visibility: hidden' and 'z-index: -1' breaks layout in iOS
-      ::after {
-        z-index: 0;
-        opacity: 0.3;
-      }
-    }
-
-    &.to-flip .back {
-      transform: rotateY(0deg);
-    }
+  // To adjust the behavior, this problem is not solved.
+  // See https://github.com/AaronCCWong/react-card-flip/issues/56
+  .react-card-front,
+  .react-card-back {
+    height: 0% !important;
   }
 }
 
@@ -357,7 +225,7 @@
 .nologin.error {
   .main .row {
     @media (min-width: 510px) {
-      .col-sm-offset-4 {
+      .offset-sm-4 {
         margin-left: calc(50% - 240px);
       }
 

+ 0 - 3
src/client/styles/scss/_me.scss

@@ -1,5 +1,2 @@
 .user-settings-page {
-  header.grw-header {
-    height: unset;
-  }
 }

+ 25 - 24
src/client/styles/scss/_override-bootstrap-variables.scss

@@ -26,33 +26,25 @@ $line-height-base: 1.42857;
 
 //== Components
 //
-//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-$border-radius-base: 0;
-$border-radius-large: 0;
-$border-radius-small: 0;
-
-
-//== Buttons
-//
-//## For each of Bootstrap's buttons, define text, background and border color.
-
-$btn-border-radius: 0;
-$btn-border-radius-lg: 0;
-$btn-border-radius-sm: 0;
-
+$border-radius:               .15rem;
+$border-radius-sm:            .1rem;
+$border-radius-lg:            .25rem;
+$border-radius-xl:            .35rem;
 
 
 //== Forms
 //
-$input-border-radius: 0;
-$input-border-radius-lg: 0;
-$input-border-radius-sm: 0;
+// TODO: remove or set value for GW-2531
+// $input-border-radius: 0;
+// $input-border-radius-lg: 0;
+// $input-border-radius-sm: 0;
 
 //== Navs
 
 $nav-link-padding-y: 0.75rem;
 $nav-link-padding-x: 1rem;
-$nav-tabs-border-radius: 0;
+// TODO: remove or set value for GW-2531
+// $nav-tabs-border-radius: 0;
 
 //== Navbar
 $navbar-padding-y: 0;
@@ -60,29 +52,38 @@ $navbar-brand-padding-y: 0;
 $navbar-nav-link-padding-x: 1rem;
 
 //== Dropdowns
-$dropdown-border-radius: 0;
+// TODO: remove or set value for GW-2531
+// $dropdown-border-radius: 0;
 
 //== card
-$card-border-radius: 0;
+// TODO: remove or set value for GW-2531
+// $card-border-radius: 0;
 $card-spacer-y: 7px;
 $card-spacer-x: 15px;
 
 //== Modals
 $modal-content-border-width: 0;
-$modal-content-border-radius: 0;
+// TODO: remove or set value for GW-2531
+// $modal-content-border-radius: 0;
 $modal-header-padding-y: 0.75rem;
 $modal-header-padding-x: 1rem;
 
 //== Alerts
-$alert-border-radius: 0;
+// TODO: remove or set value for GW-2531
+// $alert-border-radius: 0;
 
 //== Progress bar
 $progress-height: 4px;
-$progress-border-radius: 0;
+// TODO: remove or set value for GW-2531
+// $progress-border-radius: 0;
 $progress-bg: #f0f0f0;
 $progress-box-shadow: none;
 
+//== Code
+$pre-color: dummyinvalildcolor; // disable pre color specification with invalid value
+
 //== Custom Checkbox
-$custom-checkbox-indicator-border-radius: 0px;
+// TODO: remove or set value for GW-2531
+// $custom-checkbox-indicator-border-radius: 0px;
 $custom-control-indicator-focus-box-shadow: none;
 $custom-control-indicator-size: 1.2rem;

+ 2 - 9
src/client/styles/scss/_override-bootstrap.scss

@@ -39,11 +39,6 @@
     line-height: 14px;
   }
 
-  code {
-    padding: 2px 4px;
-    font-size: 90%;
-  }
-
   // Navs
   .nav-tabs {
     .nav-item {
@@ -65,24 +60,22 @@
   // card (substitute panel of bootstrap3)
   .card {
     margin-bottom: 20px;
-    border-radius: $card-border-radius;
   }
 
   .card-header {
     font-weight: 700;
     text-transform: none;
-    border-radius: $card-border-radius;
   }
 
   .card-header:first-child {
-    border-radius: $card-border-radius;
   }
 
   // Well (substitute Well of bootstrap3)
   .card.well {
     min-height: 20px;
     padding: $card-spacer-y $card-spacer-x;
-    border-radius: 3px;
+    // TODO: remove or set value for GW-2531
+    // border-radius: 3px;
   }
 
   // Dropdowns

+ 21 - 11
src/client/styles/scss/_search.scss

@@ -18,6 +18,7 @@
 
 .search-typeahead {
   position: relative;
+  width: 100%;
 
   .search-clear {
     position: absolute;
@@ -102,17 +103,25 @@
 
 // layout
 .search-top {
-  .rbt-input.form-control {
-    width: 200px;
-    transition: 0.3s ease-out;
+  .grw-search-top-fixed {
+    // centering on navbar
+    top: $grw-navbar-height / 2;
+    left: 50vw;
+    z-index: $zindex-fixed + 1;
+    transform: translate(-50%, -50%);
 
-    // focus
-    &.focus {
-      width: 300px;
-    }
+    .rbt-input.form-control {
+      width: 200px;
+      transition: 0.3s ease-out;
 
-    @include media-breakpoint-up(md) {
-      width: 300px;
+      // focus
+      &.focus {
+        width: 300px;
+      }
+
+      @include media-breakpoint-up(md) {
+        width: 300px;
+      }
     }
   }
 }
@@ -128,7 +137,8 @@
       > li {
         > a {
           padding: 2px 8px;
-          border-radius: 0;
+          // TODO: remove or set value for GW-2531
+          // border-radius: 0;
 
           &:hover {
             color: inherit;
@@ -178,7 +188,7 @@
         padding: 16px;
         font-size: 13px;
         border: solid 1px #ccc;
-        border-radius: 3px;
+        // border-radius: 3px;
       }
     }
   }

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

@@ -51,7 +51,6 @@
   $navbar-total-height: $grw-navbar-height + $grw-navbar-border-width;
   div[class$='-LayoutContainer'] {
     height: calc(100vh - #{$navbar-total-height});
-    margin-top: $navbar-total-height;
   }
   div[class$='-NavigationContainer'] {
     top: $navbar-total-height;
@@ -118,6 +117,7 @@
   }
   div[class$='-ScrollableTransitionGroup'] {
     // remove horizontal line
+    > div,
     > div > div {
       &:before,
       &:after {

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

@@ -5,48 +5,6 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   transition: all 300ms $easeInOutCubic;
 }
 
-/*
- * layout for sticky
- */
-header.grw-header {
-  top: $grw-navbar-height + $grw-navbar-border-width;
-
-  // Adjust to be on top of the growi subnavigation
-  z-index: $zindex-sticky;
-
-  height: 110px;
-  pointer-events: none; // disable pointer events for sticky
-
-  .grw-subnav {
-    overflow: unset;
-    pointer-events: all; // enable pointer events
-  }
-}
-
-/*
- * Compact Mode Switching
- */
-.grw-subnavbar {
-  &.grw-subnavbar-compact {
-    @extend %transitionForCompactMode;
-
-    h1 {
-      @include variable-font-size(18px);
-      @extend %transitionForCompactMode;
-    }
-  }
-}
-
-/*
- * Sticky Mode Switching
- */
-.grw-subnavbar {
-  &.grw-subnavbar-sticky {
-    // set transition-duration (init -> sticky)
-    transition: all 400ms linear !important;
-  }
-}
-
 /*
  * Styles
  */

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

@@ -1,8 +1,4 @@
 .tags-page {
-  header.grw-header {
-    height: unset;
-  }
-
   .list-tag-count {
     background: rgba(0, 0, 0, 0.08);
   }

+ 0 - 35
src/client/styles/scss/_user.scss

@@ -5,41 +5,6 @@ $easeInOutCubic: cubic-bezier(0.65, 0, 0.35, 1);
   transition: all 300ms $easeInOutCubic;
 }
 
-.grw-header.grw-header-user-page {
-  height: 150px;
-}
-
-/*
- * Compact Mode Switching
- */
-.grw-subnavbar.grw-subnavbar-user-page {
-  &.grw-subnavbar-compact {
-    .grw-user-page-path {
-      margin-bottom: 0;
-      font-size: 14px;
-
-      @extend %transitionForCompactMode;
-    }
-    .picture {
-      width: 62px;
-      height: 62px;
-
-      @extend %transitionForCompactMode;
-    }
-    h1 {
-      font-size: 1.5em;
-      line-height: 30px;
-
-      @extend %transitionForCompactMode;
-    }
-    .users-meta {
-      margin-left: 15px;
-
-      @extend %transitionForCompactMode;
-    }
-  }
-}
-
 /*
  * Styles
  */

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

@@ -0,0 +1,13 @@
+// import bootstrap configurations
+@import '~bootstrap/scss/functions';
+@import '~bootstrap/scss/variables';
+@import '~bootstrap/scss/mixins';
+@import '~bootstrap/scss/utilities';
+
+@import '~reveal.js/css/reveal.css';
+@import '~reveal.js/css/theme/black.css';
+
+// hljs
+.reveal {
+  @import 'hljs';
+}

+ 0 - 9
src/client/styles/scss/_wiki.scss

@@ -127,15 +127,6 @@ div.body {
     }
   }
 
-  pre {
-    white-space: pre-line;
-  }
-
-  // only inline code blocks
-  p code {
-    font-family: $font-family-monospace-not-strictly;
-  }
-
   .page-template-builder {
     position: relative;
 

+ 8 - 1
src/client/styles/scss/atoms/_buttons.scss

@@ -31,8 +31,12 @@
 }
 
 // fill button style
-.btn-fill {
+:root .btn.btn-fill {
   position: relative;
+  display: flex;
+  justify-content: space-between;
+  min-width: 130px;
+  padding: 0px;
   overflow: hidden;
   color: white;
   text-align: center;
@@ -43,6 +47,7 @@
   .btn-label {
     position: relative;
     z-index: 1;
+    padding: 9px 15px;
     color: white;
     text-decoration: none;
   }
@@ -50,7 +55,9 @@
   .btn-label-text {
     position: relative;
     z-index: 1;
+    margin: auto;
     color: white;
+    text-align: center;
     text-decoration: none;
   }
 

+ 11 - 0
src/client/styles/scss/atoms/_code.scss

@@ -0,0 +1,11 @@
+/*
+ * style of inline-code
+ */
+:not(pre) {
+  > code {
+    padding: 2px 4px;
+    font-family: $font-family-monospace-not-strictly;
+    border: 1px solid;
+    border-radius: $border-radius;
+  }
+}

+ 4 - 0
src/client/styles/scss/atoms/_pre.scss

@@ -0,0 +1,4 @@
+pre {
+  padding: 0.5em;
+  border-radius: $border-radius;
+}

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

@@ -1,5 +1,4 @@
 // import variables
-@import '../agile-admin/inverse/variables';
 @import 'variables';
 
 @import 'mixins';
@@ -16,7 +15,9 @@
 
 // atoms
 @import 'atoms/buttons';
+@import 'atoms/code';
 @import 'atoms/nav';
+@import 'atoms/pre';
 @import 'atoms/spinners';
 @import 'atoms/custom_control';
 

+ 4 - 7
src/client/styles/scss/style-presentation.scss

@@ -1,13 +1,10 @@
-// import Growi variable
+// import variable
 @import 'variables';
 
-@import '~reveal.js/css/reveal.css';
-@import '~reveal.js/css/theme/black.css';
+@import 'mixins';
+@import 'override-bootstrap-variables';
 
-// hljs
-.reveal {
-  @import 'hljs';
-}
+@import 'vendor-presentation';
 
 .reveal {
   font-size: 32px;

+ 4 - 17
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -1,3 +1,6 @@
+// determine optional variables
+$bgcolor-subnabvar: lighten($bgcolor-global, 3%) !default;
+
 /*
   * Form
   */
@@ -131,24 +134,8 @@ ul.pagination {
 /*
  * GROWI subnavigation
  */
-.admin-page,
-.user-settings-page,
-.tags-page {
-  .grw-header {
-    background-color: rgba(darken($bgcolor-global, 90%), 0.9);
-  }
-}
-
 .grw-subnavbar {
-  background-color: rgba(darken($bgcolor-global, 90%), 1);
-
-  &.grw-subnavbar-sticky {
-    background-color: rgba(darken($bgcolor-global, 90%), 0.9);
-    box-shadow: 0 3px 2px -2px darken($bgcolor-global, 5%);
-  }
-}
-
-.grw-subnavbar-sticky {
+  background-color: $bgcolor-subnabvar;
 }
 
 /*

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

@@ -123,6 +123,9 @@ body.kibela {
   }
 
   /* Modal */
+  .modal-title {
+    color: #ffffff; // override header colors
+  }
   .modal-content {
     background-color: $themelight;
   }
@@ -131,6 +134,8 @@ body.kibela {
   :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 */

+ 4 - 14
src/client/styles/scss/theme/_apply-colors-light.scss

@@ -1,3 +1,6 @@
+// determine optional variables
+$bgcolor-subnabvar: darken($bgcolor-global, 3%) !default;
+
 /*
  * Form
  */
@@ -26,21 +29,8 @@
 /*
  * GROWI subnavigation
  */
-.admin-page,
-.user-settings-page,
-.tags-page {
-  .grw-header {
-    background-color: rgba(darken($bgcolor-global, 6%), 0.9);
-  }
-}
-
 .grw-subnavbar {
-  background-color: rgba(darken($bgcolor-global, 5%), 1);
-
-  &.grw-subnavbar-sticky {
-    background-color: rgba(darken($bgcolor-global, 6%), 0.9);
-    box-shadow: 0 3px 2px -2px darken($bgcolor-global, 40%);
-  }
+  background-color: $bgcolor-subnabvar;
 }
 
 /*

+ 36 - 15
src/client/styles/scss/theme/_apply-colors.scss

@@ -13,6 +13,13 @@ $bgcolor-search-top-dropdown: $secondary !default;
 $bgcolor-sidebar-nav-item-active: darken($bgcolor-sidebar, 10%) !default;
 $text-shadow-sidebar-nav-item-active: 1px 1px 2px $primary !default;
 $bgcolor-sidebar-list-group: $bgcolor-list !default;
+$bgcolor-inline-code: #f0f0f0 !default;
+$color-inline-code: #c7254e !default;
+$bordercolor-inline-code: #ccc8c8 !default;
+$bordercolor-nav-tabs: #dee2e6 !default;
+$bordercolor-nav-tabs-hover: #e9ecef #e9ecef $bordercolor-nav-tabs !default;
+$color-nav-tabs-link-active: #495057 !default;
+$bordercolor-nav-tabs-active: $bordercolor-nav-tabs $bordercolor-nav-tabs $bgcolor-global !default;
 
 // override bootstrap variables
 $body-bg: $bgcolor-global;
@@ -20,6 +27,11 @@ $body-color: $color-global;
 $link-color: $color-link;
 $link-hover-color: $color-link-hover;
 $input-focus-color: $color-global;
+$nav-tabs-border-color: $bordercolor-nav-tabs;
+$nav-tabs-link-hover-border-color: $bordercolor-nav-tabs-hover;
+$nav-tabs-link-active-color: $color-nav-tabs-link-active;
+$nav-tabs-link-active-bg: $bgcolor-global;
+$nav-tabs-link-active-border-color: $bordercolor-nav-tabs-active;
 
 @import '~bootstrap/scss/functions';
 @import '~bootstrap/scss/variables';
@@ -30,6 +42,24 @@ $input-focus-color: $color-global;
 @import 'reboot-bootstrap-colors';
 @import 'reboot-bootstrap-theme-colors';
 @import 'reboot-toastr-colors';
+@import 'reboot-bootstrap-nav';
+
+:not(pre) {
+  > code {
+    color: $color-inline-code;
+    background-color: $bgcolor-inline-code;
+    border-color: $bordercolor-inline-code;
+  }
+}
+
+pre:not(.hljs) {
+  background-color: $bgcolor-inline-code;
+  border-color: $bordercolor-inline-code;
+}
+
+//
+//== Apply to Bootstrap Elements
+//
 
 // Link buttons
 .btn-link {
@@ -65,14 +95,13 @@ $input-focus-color: $color-global;
 }
 
 // Tabs
-.nav.nav-tabs {
-  > li > a {
-    color: $color-link;
+.nav.nav-tabs .nav-link.active {
+  color: $color-link !important;
+  background: transparent;
 
-    &:hover,
-    &:focus {
-      color: $color-link-hover;
-    }
+  &:hover,
+  &:focus {
+    color: $color-link-hover !important;
   }
 }
 
@@ -177,14 +206,6 @@ $input-focus-color: $color-global;
   fill: $color-editor-icons;
 }
 
-/*
- * code color of inline-code
- */
-:not(.hljs) > code:not(.hljs) {
-  color: $color-inline-code;
-  background-color: $bgcolor-inline-code;
-}
-
 /*
  * Modal
  */

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

@@ -25,9 +25,7 @@ body {
   background-color: $body-bg; // 2
 }
 
-//
 // Links
-//
 
 a {
   color: $link-color;

+ 1 - 0
src/client/styles/scss/theme/_reboot-bootstrap-nav.scss

@@ -0,0 +1 @@
+@import '~bootstrap/scss/nav';

+ 21 - 11
src/client/styles/scss/theme/antarctic.scss

@@ -49,19 +49,19 @@ html[dark] {
 
   // Background colors
   $bgcolor-global: $themelight;
-  $bgcolor-inline-code: #f9f2f4;
+  $bgcolor-inline-code: #f0f0f0; //optional
   $bgcolor-card: #f5f5f5;
 
   // Font colors
   $color-global: black;
   $color-reversal: #eeeeee;
   // $color-header: #2b2b2b;
-  $color-link: lighten($color-global, 20%);
+  $color-link: lighten($themecolor, 20%);
   $color-link-hover: lighten($color-link, 20%);
   $color-link-wiki: lighten($primary, 20%);
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e;
+  $color-inline-code: #c7254e; // optional
 
   // List Group colors
   $color-list: $color-global;
@@ -71,24 +71,34 @@ html[dark] {
   $color-list-hover: $color-reversal;
 
   // Navbar
-  $bgcolor-navbar: $themecolor;
-  $border-color-navbar-gradient-left: #545fff;
-  $border-color-navbar-gradient-right: #00a6e5;
-
+  $bgcolor-navbar: #35393f;
+  $bgcolor-search-top-dropdown: #fa9913;
+  $border-image-navbar: linear-gradient(to right, #f6d02e 0%, #f87c00 47%, #f6d02e 100%);
   // Logo colors
   $bgcolor-logo: $bgcolor-navbar;
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
 
   // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-navbar, 10%);
+  $bgcolor-sidebar: $themecolor;
+  $bgcolor-sidebar-nav-item-active: rgba(#000000, 0.37); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-reversal;
+  $bgcolor-resize-button: #fa9913;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $color-sidebar-context: $themecolor;
+  $bgcolor-sidebar-context: #f4f6fc;
+  // Sidebar list group
+  $bgcolor-sidebar-list-group: #fafbff; // optional
 
   // Icon colors
   $color-editor-icons: $color-global;
 
   // Border colors
   $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;
@@ -134,7 +144,7 @@ html[dark] {
 //   $color-link-wiki: lighten($basecolor, 50%);
 //   $color-link-wiki-hover: darken($color-link-wiki, 5%);
 //   $color-link-nabvar: $color-global;
-//   $color-inline-code: #c7254e;
+//   $color-inline-code: #c7254e; // optional
 
 //   // List Group colors
 //   $color-list: $color-global;

+ 3 - 2
src/client/styles/scss/theme/christmas.scss

@@ -23,8 +23,6 @@ $sidebar-text: #ffffff;
 $fillcolor-logo-mark: lighten(desaturate($themecolor, 50%), 50%);
 $color-link-wiki: lighten($subthemecolor, 5%);
 $color-link-wiki-hover: lighten($color-link-wiki, 15%);
-$color-inline-code: darken($subthemecolor, 5%);
-$bgcolor-inline-code: lighten($subthemecolor, 70%);
 $active-nav-tabs-bgcolor: white;
 
 .growi:not(.login-page) {
@@ -45,6 +43,7 @@ html[dark] {
   $primary: #d3c665;
   // Background colors
   $bgcolor-card: #f5f5f5;
+  $bgcolor-inline-code: #f0f0f0; //optional
 
   // Font colors
   $color-global: #112744;
@@ -52,6 +51,7 @@ html[dark] {
   $color-link: $subthemecolor;
   $color-link-hover: lighten($color-link, 10%);
   $color-link-nabvar: $color-reversal;
+  $color-inline-code: #c7254e; // optional
 
   // List Group colors
   $color-list: $color-global;
@@ -89,6 +89,7 @@ html[dark] {
 
   // Border colors
   $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;

+ 28 - 8
src/client/styles/scss/theme/default.scss

@@ -19,7 +19,7 @@ html[light] {
 
   // Background colors
   $bgcolor-global: white;
-  $bgcolor-inline-code: #f9f2f4;
+  $bgcolor-inline-code: #f0f0f0; //optional
   $bgcolor-card: #f5f5f5;
 
   // Font colors
@@ -31,7 +31,7 @@ html[light] {
   $color-link-wiki: $color-link;
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-nabvar: #a7a7a7;
-  $color-inline-code: #c7254e;
+  $color-inline-code: #c7254e; // optional
 
   // List Group colors
   // $color-list: $color-global; // optional
@@ -64,11 +64,21 @@ html[light] {
   // Sidebar list group
   $bgcolor-sidebar-list-group: #fafbff; // optional
 
+  // Subnavigation
+  // $bgcolor-subnabvar: #fafafa; // optional
+
+  // Tabs
+  // $bordercolor-nav-tabs: #; // optional
+  // $color-nav-tabs-link-active: #; //optional
+  // $bordercolor-nav-tabs-hover: # # $bordercolor-nav-tabs; // optional
+  // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
+
   // Icon colors
   $color-editor-icons: $color-global;
 
   // Border colors
-  $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $border-color-theme: #ccc;
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;
@@ -95,7 +105,7 @@ html[dark] {
 
   // Background colors
   $bgcolor-global: #131418;
-  $bgcolor-inline-code: darken($bgcolor-global, 5%);
+  $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: darken($bgcolor-global, 5%);
 
   // Font colors
@@ -107,7 +117,7 @@ html[dark] {
   $color-link-wiki: $color-link;
   $color-link-wiki-hover: lighten($color-link-wiki, 10%);
   $color-link-nabvar: #a7a7a7;
-  $color-inline-code: #c7254e;
+  $color-inline-code: #c7254e; // optional
 
   // List Group colors
   // $color-list: $color-global; // optional
@@ -130,9 +140,9 @@ html[dark] {
   $bgcolor-sidebar-nav-item-active: rgba(#969494, 0.3); // optional
   $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
   // Sidebar resize button
-  $color-resize-button: $color-global;
+  $color-resize-button: white;
   $bgcolor-resize-button: $primary;
-  $color-resize-button-hover: $color-global;
+  $color-resize-button-hover: white;
   $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
   // Sidebar contents
   $bgcolor-sidebar-context: #111d2f;
@@ -140,11 +150,21 @@ html[dark] {
   // Sidebar list group
   $bgcolor-sidebar-list-group: #1c2a3e; // optional
 
+  // Subnavigation
+  $bgcolor-subnabvar: lighten($bgcolor-global, 4%); // optional
+
+  // Tabs
+  $bordercolor-nav-tabs: #444; // optional
+  // $color-nav-tabs-link-active: #; //optional
+  $bordercolor-nav-tabs-hover: #666 #666 $bordercolor-nav-tabs; // optional
+  // $bordercolor-nav-tabs-active: # # $bgcolor-global; // optional
+
   // Icon colors
   $color-editor-icons: $color-global;
 
   // Border colors
-  $border-color-theme: black; // former: `$navbar-border: #ccc;`
+  $border-color-theme: black;
+  $bordercolor-inline-code: #4d4d4d; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;

+ 3 - 2
src/client/styles/scss/theme/future.scss

@@ -12,7 +12,7 @@ html[light],
 html[dark] {
   // Background colors
   $bgcolor-global: $themecolor;
-  $bgcolor-inline-code: darken($themecolor, 5%);
+  $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: darken($themecolor, 5%);
 
   // Font colors
@@ -24,7 +24,7 @@ html[dark] {
   $color-link-wiki: darken($themecolor, 5%);
   $color-link-wiki-hover: darken($color-link-wiki, 5%);
   $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e;
+  $color-inline-code: #c7254e; // optional
   $color-search: #050a0b;
 
   // List Group colors
@@ -53,6 +53,7 @@ html[dark] {
 
   // Border colors
   $border-color-theme: #407483;
+  $bordercolor-inline-code: #4d4d4d; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;

+ 3 - 2
src/client/styles/scss/theme/halloween.scss

@@ -36,7 +36,7 @@ html[light],
 html[dark] {
   // Background colors
   $bgcolor-global: #050000;
-  $bgcolor-inline-code: #f9f2f4;
+  $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: #f5f5f5;
 
   // Font colors
@@ -48,7 +48,7 @@ html[dark] {
   $color-link-wiki: lighten($primary, 20%);
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e;
+  $color-inline-code: #c7254e; // optional
 
   // List Group colors
   $color-list: $color-global;
@@ -76,6 +76,7 @@ html[dark] {
 
   // Border colors
   $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $bordercolor-inline-code: #4d4d4d; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;

+ 3 - 0
src/client/styles/scss/theme/island.scss

@@ -19,12 +19,14 @@ html[dark] {
   // Background colors
   $bgcolor-card: #f5f5f5;
   $bgcolor-global: lighten($color-themelight, 10%);
+  $bgcolor-inline-code: #f0f0f0; //optional
 
   // Font colors
   $color-reversal: #eeeeee;
   $color-link: lighten($color-global, 20%);
   $color-link-hover: lighten($color-link, 20%);
   $color-link-nabvar: $color-reversal;
+  $color-inline-code: #c7254e; // optional
 
   // List Group colors
   $color-list: $color-global;
@@ -52,6 +54,7 @@ html[dark] {
 
   // Border colors
   $border-color-theme: #ccc;
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;

+ 16 - 67
src/client/styles/scss/theme/kibela.scss

@@ -7,7 +7,8 @@ $subthemecolor: rgb(88, 130, 250);
 $lightthemecolor: rgba(181, 203, 247, 0.61);
 
 // Light Mode
-html[light] {
+html[light],
+html[dark] {
   // Background colors
   $bgcolor-navbar: white;
   $bgcolor-navbar-active: $bgcolor-theme;
@@ -31,71 +32,8 @@ html[light] {
   $bgcolor-list-active: $primary;
   $color-list-hover: $color-reversal;
 
-  // Logo colors
-  $bgcolor-logo: transparent;
-  $fillcolor-logo-mark: lighten($bgcolor-theme, 20%);
-
-  // Icon colors
-  $color-editor-icons: $color-global;
-
-  $color-link-wiki: lighten($bgcolor-theme, 20%);
-  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: $color-global;
-  $color-link-nabvar-hover: $color-global;
-  $color-inline-code: $subthemecolor;
-
-  // border colors
-  $border-color-theme: $lightthemecolor;
-  $thickborder: #5584e1;
-
-  // dropdown colors
-  $bgcolor-dropdown-link-active: $growi-blue;
-  $color-dropdown-link-active: $color-reversal;
-  $color-dropdown-link-hover: $color-global;
-
-  // admin theme box
-  $color-theme-color-box: lighten($bgcolor-theme, 20%);
-
-  // alert
-  $color-alert: $color-reversal;
-
-  // badge
-  $color-badge: $color-reversal;
-
-  // Sidebar
-  $bgcolor-sidebar: $bgcolor-theme;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-theme, 10%);
-
-  @import 'apply-colors';
-  @import 'apply-colors-light';
-  @import 'apply-colors-kibela';
-}
-
-// Dark Mode ( same as Light Mode )
-html[dark] {
-  // Background colors
-  $bgcolor-navbar: white;
-  $bgcolor-navbar-active: $bgcolor-theme;
-  $bgcolor-global: $themelight;
-  $bgcolor-card: $lightthemecolor;
-  $bgcolor-inline-code: lighten($subthemecolor, 70%);
-  $color-header: $bgcolor-theme;
-  $color-global: #3c4a60;
-  $color-link: rgb(74, 109, 204);
-  $color-link-hover: lighten($color-link, 12%);
-  $sidebar-text: $bgcolor-theme;
-  $color-reversal: #eee;
-
-  $primary: $bgcolor-theme;
-  $info: lighten($bgcolor-theme, 20%);
-
-  // List Group colors
-  $color-list: $color-global;
-  $bgcolor-list: $bgcolor-global;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  // navbar
+  $bgcolor-search-top-dropdown: $primary;
 
   // Logo colors
   $bgcolor-logo: transparent;
@@ -113,6 +51,7 @@ html[dark] {
   // border colors
   $border-color-theme: $lightthemecolor;
   $thickborder: #5584e1;
+  $bordercolor-inline-code: $lightthemecolor;
 
   // dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;
@@ -131,7 +70,17 @@ html[dark] {
   // Sidebar
   $bgcolor-sidebar: $bgcolor-theme;
   $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-theme, 5%);
+  $bgcolor-sidebar-context: lighten($bgcolor-theme, 10%);
+  // Sidebar resize button
+  $color-resize-button: $color-reversal;
+  $bgcolor-resize-button: #209fd8;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $color-sidebar-context: $color-global;
+  $bgcolor-sidebar-context: #f4f6fc;
+  // Sidebar list group
+  $bgcolor-sidebar-list-group: #fafbff; // optional
 
   @import 'apply-colors';
   @import 'apply-colors-light';

+ 54 - 43
src/client/styles/scss/theme/mono-blue.scss

@@ -12,8 +12,7 @@ html[light] {
 
   // Background colors
   $bgcolor-global: $themelight;
-  $bgcolor-navbar: $themecolor;
-  $bgcolor-inline-code: lighten($subthemecolor, 70%);
+  $bgcolor-inline-code: #f0f0f0; //optional
   $bgcolor-card: darken($themelight, 5%);
 
   // Font colors
@@ -24,7 +23,7 @@ html[light] {
   $color-link-wiki: lighten($primary, 20%);
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-nabvar: $color-reversal;
-  $color-inline-code: $subthemecolor;
+  $color-inline-code: #c7254e; // optional
   $color-search: #c0d6df;
 
   // List Group colors
@@ -32,17 +31,38 @@ html[light] {
   $bgcolor-list: transparent;
   $color-list-active: $color-reversal;
   $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  $color-list-hover: $color-search;
+
+  // Navbar
+  $bgcolor-navbar: #2a2929;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
 
   // Logo colors
   $bgcolor-logo: $themecolor;
-  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 30%), 20%);
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
+
+  // Sidebar
+  $bgcolor-sidebar: $themecolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.37); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-reversal;
+  $bgcolor-resize-button: #209fd8;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $color-sidebar-context: $color-global;
+  $bgcolor-sidebar-context: #f1fcff;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
 
   // Icon colors
   $color-editor-icons: $color-global;
 
   // Border colors
   $border-color-theme: #ccc;
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;
@@ -57,11 +77,6 @@ html[light] {
   // badge
   $color-badge: $color-reversal;
 
-  // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
-
   @import 'apply-colors';
   @import 'apply-colors-light';
 
@@ -78,33 +93,22 @@ html[light] {
       }
     }
   }
-
-  // Search Top
-  .search-top {
-    .input-group-prepend .dropdown-toggle {
-      color: $themecolor;
-      background-color: $color-search;
-      &:hover {
-        background-color: darken($color-search, 10%);
-      }
-    }
-  }
 }
 
 html[dark] {
   // Theme colors
-  $themecolor: #0090c8;
+  $themecolor: #00587a;
   $themedark: #061f2f;
   $accentcolor: #16617d;
   $subthemecolor: #c1f1f0;
 
-  $primary: $themecolor;
+  $primary: #0090c8;
   $dark: #031018;
 
   // Background colors
   $bgcolor-global: $themedark;
   $bgcolor-navbar: #27343b;
-  $bgcolor-inline-code: #0a121b;
+  $bgcolor-inline-code: #1f1f22; //optional
   $bgcolor-card: darken($themedark, 5%);
 
   // Font colors
@@ -116,6 +120,7 @@ html[dark] {
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-nabvar: $color-reversal;
   $color-inline-code: $subthemecolor;
+  $color-inline-code: #c7254e; // optional
   $color-search: #000102;
 
   // List Group colors
@@ -123,22 +128,44 @@ html[dark] {
   $bgcolor-list: transparent;
   $color-list-active: $color-reversal;
   $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  $color-list-hover: $accentcolor;
+
+  // Navbar
+  $bgcolor-navbar: #2a2929;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #54bafd 0%, #3d98a3 50%, #708b0b 100%);
 
   // Logo colors
   $bgcolor-logo: #13191c;
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 10%), 15%);
   // $fillcolor-logo-mark: #4e5a60;
 
+  // Sidebar
+  $bgcolor-sidebar: $accentcolor;
+  // $bgcolor-sidebar-nav-item-active: rgba(#, 0.3); // optional
+  $text-shadow-sidebar-nav-item-active: 0px 0px 10px #0099ff; // optional
+  // Sidebar resize button
+  $color-resize-button: $color-global;
+  $bgcolor-resize-button: $themecolor;
+  $color-resize-button-hover: $color-global;
+  $bgcolor-resize-button-hover: darken($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $bgcolor-sidebar-context: darken($bgcolor-sidebar, 13%);
+  $color-sidebar-context: $color-global;
+  // Sidebar list group
+  // $bgcolor-sidebar-list-group: #; // optional
+
   // Icon colors
   $color-editor-icons: $color-global;
 
   // Border colors
   $border-color-theme: #146aa0;
+  $bordercolor-inline-code: #4d4d4d; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $primary;
-  $color-dropdown-link-active: $color-reversal;
+  $color-dropdown-link-active: $color-global;
+  $color-dropdown-link-hover: $color-reversal;
 
   // admin theme box
   $color-theme-color-box: $primary;
@@ -149,15 +176,10 @@ html[dark] {
   // badge
   $color-badge: $color-reversal;
 
-  // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
-
   @import 'apply-colors';
   @import 'apply-colors-dark';
 
-  // Navs {
+  // Navs
   .nav-tabs {
     border-bottom: $border-color-theme 1px solid;
     .nav-link {
@@ -173,17 +195,6 @@ html[dark] {
     }
   }
 
-  // Search Top
-  .search-top {
-    .input-group-prepend .dropdown-toggle {
-      background-color: $color-search;
-      border-color: $color-search;
-      &:hover {
-        background-color: darken($color-search, 10%);
-      }
-    }
-  }
-
   // Table
   .table {
     color: white;

+ 41 - 40
src/client/styles/scss/theme/nature.scss

@@ -31,71 +31,56 @@
   }
 }
 
-$themecolor: #118050;
-$themelight: #fefffd;
+$themecolor: #12b105;
 
 //== Light Mode
 //
 html[light],
 html[dark] {
-  $bgcolor-theme: #460039;
-
-  $bgcolor-navbar: #118050;
-  $bgcolor-global: #fefffd;
-
-  $color-header: #46694e;
-  $color-global: #333333;
-  $linktext: lighten($bgcolor-theme, 5%);
-  $linktext-hover: lighten($linktext, 12%);
-  $sidebar-text: #5c7253;
-
-  $primary: $bgcolor-theme;
-
-  $fillcolor-logo-mark: lighten(desaturate($bgcolor-navbar, 30%), 20%);
-  $color-link-wiki: lighten($bgcolor-theme, 20%);
-  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $primary: #460039;
+  $light: #f0f0f0;
 
   // Background colors
-  $bgcolor-global: $themelight;
-  $bgcolor-inline-code: #f9f2f4;
-  $bgcolor-card: #f5f5f5;
+  $bgcolor-global: #fdfdfd;
+  $bgcolor-inline-code: #f0f0f0; //optional
+  $bgcolor-card: #f1ffe4;
+  $bgcolor-subnabvar: #fafafa;
 
   // Font colors
-  $color-global: $bgcolor-theme;
+  $color-global: #460039;
   $color-reversal: #eeeeee;
-  $color-link: lighten($color-global, 20%);
+  $color-link: #7e0044;
   $color-link-hover: lighten($color-link, 20%);
   $color-link-wiki: lighten($primary, 20%);
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: #c7254e;
-
-  // List Group colors
-  $color-list: $color-global;
-  $bgcolor-list: $bgcolor-global;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  $color-link-nabvar: #a7a7a7;
+  $color-inline-code: #c7254e; // optional
+  $color-search: white;
 
   // Navbar
-  $bgcolor-navbar: $themecolor;
-  $border-color-navbar-gradient-left: #545fff;
-  $border-color-navbar-gradient-right: #00a6e5;
+  $bgcolor-navbar: #234136;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #5c78ef 0%, #16bc42 50%, #5c78ef 100%);
 
   // Logo colors
   $bgcolor-logo: $bgcolor-navbar;
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
 
   // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-navbar, 10%);
+  $bgcolor-sidebar: #188f64;
+  // Sidebar contents
+  $color-sidebar-context: #7e0044;
+  $bgcolor-sidebar-context: #fdffeb;
+  // Sidebar resize button
+  $color-resize-button: white;
+  $bgcolor-resize-button: $themecolor;
 
   // Icon colors
   $color-editor-icons: $color-global;
 
   // Border colors
   $border-color-theme: #ccc;
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;
@@ -114,8 +99,24 @@ html[dark] {
   @import 'apply-colors';
   @import 'apply-colors-light';
 
-  .table {
-    background-color: $themelight;
+  // hljs
+  .hljs {
+    background-color: $bgcolor-inline-code;
+  }
+
+  // Search Top
+  .search-top {
+    .btn-secondary.dropdown-toggle {
+      color: $color-search;
+    }
+  }
+
+  // Navs
+  .nav-tabs .nav-link.active {
+    color: $color-link !important;
+    &:hover {
+      color: $color-link-hover !important;
+    }
   }
 }
 

+ 21 - 12
src/client/styles/scss/theme/spring.scss

@@ -48,22 +48,22 @@ $accentcolor: #e08dbc;
 html[light],
 html[dark] {
   $primary: $themecolor;
+  $secondary: $accentcolor;
 
   // Background colors
   $bgcolor-global: white;
-  $bgcolor-inline-code: #f9f2f4;
+  $bgcolor-inline-code: #f0f0f0; //optional
   $bgcolor-card: #f5f5f5;
 
   // Font colors
   $color-global: black;
   $color-reversal: white;
-  // $color-header: #2b2b2b;
-  $color-link: lighten($color-global, 20%);
-  $color-link-hover: $subthemecolor;
+  $color-link: $subthemecolor;
+  $color-link-hover: lighten($subthemecolor, 10%);
   $color-link-wiki: $subthemecolor;
-  $color-link-wiki-hover: lighten($color-link-wiki, 20%);
+  $color-link-wiki-hover: lighten($color-link-wiki, 10%);
   $color-link-nabvar: $bgcolor-global;
-  $color-inline-code: #c7254e;
+  $color-inline-code: #c7254e; // optional
 
   // List Group colors
   $color-list: $color-global;
@@ -73,24 +73,33 @@ html[dark] {
   $color-list-hover: lighten($accentcolor, 20%);
 
   // Navbar
-  $bgcolor-navbar: $themecolor;
-  $border-color-navbar-gradient-left: #545fff;
-  $border-color-navbar-gradient-right: #00a6e5;
+  $bgcolor-navbar: #d3687c;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #cbe682 0%, #4ad6e8 50%, #ea42f0 100%);
 
   // Logo colors
   $bgcolor-logo: $bgcolor-navbar;
   $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
 
   // Sidebar
-  $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-navbar, 10%);
+  $bgcolor-sidebar: $themecolor;
+  // Sidebar resize button
+  $color-resize-button: $color-reversal;
+  $bgcolor-resize-button: $subthemecolor;
+  $color-resize-button-hover: $color-reversal;
+  $bgcolor-resize-button-hover: lighten($bgcolor-resize-button, 5%);
+  // Sidebar contents
+  $color-sidebar-context: $subthemecolor;
+  $bgcolor-sidebar-context: #f4f6fc;
+  // Sidebar list group
+  $bgcolor-sidebar-list-group: #fafbff; // optional
 
   // Icon colors
   $color-editor-icons: $color-global;
 
   // Border colors
   $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;

+ 50 - 26
src/client/styles/scss/theme/wood.scss

@@ -31,43 +31,39 @@
   }
 }
 
-$themecolor: #aaa45f;
+$themecolor: #b9b177;
 $themelight: #f5f3ee;
-$accentcolor: #577254;
 
 //== Light Mode
 //
 html[light],
 html[dark] {
-  $primary: $themecolor;
+  $primary: #aaa45f;
 
   // Background colors
-  $bgcolor-global: $themelight;
-  $bgcolor-inline-code: darken($themecolor, 20%);
-  $bgcolor-card: #f5f5f5;
+  $bgcolor-global: #ffffff;
+  $bgcolor-inline-code: #f0f0f0; //optional
+  $bgcolor-card: #ece8de;
 
   // Font colors
-  $color-global: black;
+  // $color-global: black;
+  $color-global: #433005;
   $color-reversal: #fffffc;
-  // $color-header: #2b2b2b;
-  $color-link: lighten($color-global, 20%);
+  $color-link: #9d7406;
   $color-link-hover: lighten($color-link, 20%);
   $color-link-wiki: lighten($themecolor, 5%);
   $color-link-wiki-hover: lighten($color-link-wiki, 15%);
-  $color-link-nabvar: $color-reversal;
-  $color-inline-code: lighten($accentcolor, 70%);
+  $color-link-nabvar: #a7a7a7;
+  $color-inline-code: #c7254e; // optional
+  $color-search: white;
 
   // List Group colors
-  $color-list: $color-global;
-  $bgcolor-list: $bgcolor-global;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  $color-list-hover: #eee;
 
   // Navbar
-  $bgcolor-navbar: $themecolor;
-  $border-color-navbar-gradient-left: #545fff;
-  $border-color-navbar-gradient-right: #00a6e5;
+  $bgcolor-navbar: #2a2929;
+  $bgcolor-search-top-dropdown: $themecolor;
+  $border-image-navbar: linear-gradient(to right, #5c78ef 0%, #16bc42 50%, #5c78ef 100%);
 
   // Logo colors
   $bgcolor-logo: darken($themecolor, 10%);
@@ -76,11 +72,18 @@ html[dark] {
 
   // Sidebar
   $bgcolor-sidebar: $bgcolor-navbar;
-  $color-sidebar-context: $color-reversal;
-  $bgcolor-sidebar-context: lighten($bgcolor-navbar, 10%);
+  // Sidebar contents
+  $color-sidebar-context: #9d7406;
+  $bgcolor-sidebar-context: #f0efe7;
+  // Sidebar list group
+  $bgcolor-sidebar-list-group: #f7f5f1;
+  // Sidebar resize button
+  $color-resize-button: white;
+  $bgcolor-resize-button: $themecolor;
 
   // Border colors
   $border-color-theme: #ccc; // former: `$navbar-border: #ccc;`
+  $bordercolor-inline-code: #ccc8c8; // optional
 
   // Dropdown colors
   $bgcolor-dropdown-link-active: $growi-blue;
@@ -102,13 +105,34 @@ html[dark] {
   @import 'apply-colors';
   @import 'apply-colors-light';
 
-  .table {
-    background-color: $themelight;
+  // hljs
+  .hljs {
+    color: #433005;
+    background-color: $bgcolor-inline-code;
   }
 
-  .grw-navbar {
-    background-image: url('/images/themes/wood/wood-navbar.jpg');
-    border-bottom: $accentcolor 4px solid;
+  // List
+  .list-group .list-group-item {
+    background: transparent;
+  }
+
+  // Search Top
+  .search-top {
+    .btn-secondary.dropdown-toggle {
+      color: $color-search;
+    }
+  }
+
+  // Sidebar
+  .grw-sidebar div[data-testid='GlobalNavigation'] {
+    * {
+      background-image: url('/images/themes/wood/wood-navbar.jpg');
+    }
+
+    button,
+    .btn {
+      border: none;
+    }
   }
 }
 

+ 4 - 2
src/lib/models/devided-page-path.js

@@ -1,11 +1,11 @@
-import { pathUtils } from 'growi-commons';
+const { pathUtils } = require('growi-commons');
 
 // https://regex101.com/r/BahpKX/2
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
 // https://regex101.com/r/WVpPpY/1
 const PATTERN_DEFAULT = /^((.*)\/)?([^/]+)$/;
 
-export default class DevidedPagePath {
+class DevidedPagePath {
 
   constructor(path, skipNormalize = false, evalDatePath = false) {
 
@@ -43,3 +43,5 @@ export default class DevidedPagePath {
   }
 
 }
+
+module.exports = DevidedPagePath;

+ 6 - 4
src/lib/models/linked-page-path.js

@@ -1,12 +1,12 @@
-import { pathUtils } from 'growi-commons';
-import { isTrashPage } from '@commons/util/path-utils';
+const { pathUtils } = require('growi-commons');
+const { isTrashPage } = require('@commons/util/path-utils');
 
-import DevidedPagePath from './devided-page-path';
+const DevidedPagePath = require('./devided-page-path');
 
 /**
  * Linked Array Structured PagePath Model
  */
-export default class LinkedPagePath {
+class LinkedPagePath {
 
   constructor(path, skipNormalize = false) {
 
@@ -34,3 +34,5 @@ export default class LinkedPagePath {
   }
 
 }
+
+module.exports = LinkedPagePath;

+ 12 - 11
src/server/routes/index.js

@@ -63,17 +63,17 @@ module.exports = function(crowi, app) {
   app.get('/admin/security'                     , loginRequiredStrictly , adminRequired , admin.security.index);
 
   // OAuth
-  app.get('/passport/google'                      , loginPassport.loginWithGoogle);
-  app.get('/passport/github'                      , loginPassport.loginWithGitHub);
-  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter);
-  app.get('/passport/oidc'                        , loginPassport.loginWithOidc);
-  app.get('/passport/saml'                        , loginPassport.loginWithSaml);
-  app.get('/passport/basic'                       , loginPassport.loginWithBasic);
-  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback);
-  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback);
-  app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback);
-  app.get('/passport/oidc/callback'               , loginPassport.loginPassportOidcCallback);
-  app.post('/passport/saml/callback'              , loginPassport.loginPassportSamlCallback);
+  app.get('/passport/google'                      , loginPassport.loginWithGoogle, loginPassport.loginFailure);
+  app.get('/passport/github'                      , loginPassport.loginWithGitHub, loginPassport.loginFailure);
+  app.get('/passport/twitter'                     , loginPassport.loginWithTwitter, loginPassport.loginFailure);
+  app.get('/passport/oidc'                        , loginPassport.loginWithOidc, loginPassport.loginFailure);
+  app.get('/passport/saml'                        , loginPassport.loginWithSaml, loginPassport.loginFailure);
+  app.get('/passport/basic'                       , loginPassport.loginWithBasic, loginPassport.loginFailure);
+  app.get('/passport/google/callback'             , loginPassport.loginPassportGoogleCallback   , loginPassport.loginFailure);
+  app.get('/passport/github/callback'             , loginPassport.loginPassportGitHubCallback   , loginPassport.loginFailure);
+  app.get('/passport/twitter/callback'            , loginPassport.loginPassportTwitterCallback  , loginPassport.loginFailure);
+  app.get('/passport/oidc/callback'               , loginPassport.loginPassportOidcCallback     , loginPassport.loginFailure);
+  app.post('/passport/saml/callback'              , loginPassport.loginPassportSamlCallback     , loginPassport.loginFailure);
 
   // markdown admin
   app.get('/admin/markdown'                   , loginRequiredStrictly , adminRequired , admin.markdown.index);
@@ -182,4 +182,5 @@ module.exports = function(crowi, app) {
 
   app.get('/*/$'                   , loginRequired , page.showPageWithEndOfSlash, page.notFound);
   app.get('/*'                     , loginRequired , page.showPage, page.notFound);
+
 };

+ 9 - 8
src/server/routes/login-passport.js

@@ -89,7 +89,8 @@ module.exports = function(crowi, app) {
       ldapAccountInfo = await promisifiedPassportAuthentication(strategyName, req, res);
     }
     catch (err) {
-      return next(err);
+      debug(err.message);
+      return next();
     }
 
     // check groups for LDAP
@@ -125,7 +126,7 @@ module.exports = function(crowi, app) {
 
     // login
     await req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -216,7 +217,7 @@ module.exports = function(crowi, app) {
       }
       if (!user) { return next() }
       req.logIn(user, (err) => {
-        if (err) { return next() }
+        if (err) { debug(err.message); return next() }
 
         return loginSuccessHandler(req, res, user);
       });
@@ -287,7 +288,7 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -329,7 +330,7 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -371,7 +372,7 @@ module.exports = function(crowi, app) {
 
     // login
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -419,7 +420,7 @@ module.exports = function(crowi, app) {
     // login
     const user = await externalAccount.getPopulatedUser();
     req.logIn(user, (err) => {
-      if (err) { return next(err) }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };
@@ -523,7 +524,7 @@ module.exports = function(crowi, app) {
 
     const user = await externalAccount.getPopulatedUser();
     await req.logIn(user, (err) => {
-      if (err) { return next() }
+      if (err) { debug(err.message); return next() }
       return loginSuccessHandler(req, res, user);
     });
   };

+ 1 - 1
src/server/routes/page.js

@@ -1296,7 +1296,7 @@ module.exports = function(crowi, app) {
     const socketClientId = req.body.socketClientId || undefined;
 
     // get recursively flag
-    const isRecursively = (req.body.recursively !== undefined);
+    const isRecursively = (req.body.recursively != null);
 
     let page;
     try {

+ 22 - 4
src/server/service/customize.js

@@ -1,4 +1,7 @@
-const logger = require('@alias/logger')('growi:service:CustomizeService'); // eslint-disable-line no-unused-vars
+// eslint-disable-next-line no-unused-vars
+const logger = require('@alias/logger')('growi:service:CustomizeService');
+
+const DevidedPagePath = require('@commons/models/devided-page-path');
 
 /**
  * the service class of CustomizeService
@@ -35,17 +38,32 @@ class CustomizeService {
     let configValue = this.configManager.getConfig('crowi', 'customize:title');
 
     if (configValue == null || configValue.trim().length === 0) {
-      configValue = '{{page}} - {{sitename}}';
+      configValue = '{{pagename}} - {{sitename}}';
     }
 
     this.customTitleTemplate = configValue;
   }
 
-  generateCustomTitle(page) {
+  generateCustomTitle(pageOrPath) {
+    const path = pageOrPath.path || pageOrPath;
+    const dPagePath = new DevidedPagePath(path, true, true);
+
+    const customTitle = this.customTitleTemplate
+      .replace('{{sitename}}', this.appService.getAppTitle())
+      .replace('{{pagepath}}', path)
+      .replace('{{page}}', dPagePath.latter) // for backward compatibility
+      .replace('{{pagename}}', dPagePath.latter);
+
+    return this.xssService.process(customTitle);
+  }
+
+  generateCustomTitleForFixedPageName(title) {
     // replace
     const customTitle = this.customTitleTemplate
       .replace('{{sitename}}', this.appService.getAppTitle())
-      .replace('{{page}}', page);
+      .replace('{{page}}', title)
+      .replace('{{pagepath}}', title)
+      .replace('{{pagename}}', title);
 
     return this.xssService.process(customTitle);
   }

+ 1 - 1
src/server/views/admin/app.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('App Settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('App Settings')) }}{% endblock %}
 
 {% block head_warn_alert_siteurl_undefined %} {# remove including block for './widget/alert_siteurl_undefined.html' #}
 {% endblock %}

+ 4 - 1
src/server/views/admin/customize.html

@@ -1,5 +1,5 @@
 {% extends '../layout/admin.html' %}
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Customize')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Customize')) }}{% endblock %}
 
 {% block html_additional_headers %}
 {% parent %}
@@ -19,6 +19,9 @@
 {% block content_main %}
 <div class="content-main admin-customize row">
   {% parent %}
+  <div id="grw-hljs-container-for-demo">
+    {{ cdnHighlightJsStyleTag(getConfig('crowi', 'customize:highlightJsStyle')) }}
+  </div>
   <div class="col-lg-9" id="admin-customize"></div>
 </div>
 {% endblock content_main %}

+ 1 - 1
src/server/views/admin/export.html

@@ -1,6 +1,6 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Export Archive Data')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitleForFixedPageName(t('Export Archive Data')) }}{% endblock %}
 
 {% block content_header %}
 <h1 class="title">{{ t('Export Archive Data') }}</h1>

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini