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

Merge branch 'support/reactify-pagemanagement-modal-for-merge' into imprv/renamepage-modal-reactify

# Conflicts:
#	src/client/js/bootstrap.jsx
#	src/client/js/services/PageContainer.js
ryohek 5 лет назад
Родитель
Сommit
b6a2a21096
49 измененных файлов с 699 добавлено и 873 удалено
  1. 13 4
      CHANGES.md
  2. 1 1
      package.json
  3. 1 1
      resource/cdn-manifests.js
  4. 112 111
      resource/locales/en-US/sandbox.md
  5. 4 2
      resource/locales/en-US/translation.json
  6. 4 2
      resource/locales/ja/translation.json
  7. 2 0
      src/client/js/bootstrap.jsx
  8. 2 2
      src/client/js/components/Admin/Common/AdminNavigation.jsx
  9. 1 1
      src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx
  10. 1 1
      src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx
  11. 2 2
      src/client/js/components/Admin/Security/OidcSecuritySetting.jsx
  12. 1 1
      src/client/js/components/Admin/Security/SamlSecuritySetting.jsx
  13. 1 1
      src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx
  14. 2 2
      src/client/js/components/Admin/UserManagement.jsx
  15. 2 2
      src/client/js/components/Admin/Users/UserTable.jsx
  16. 87 0
      src/client/js/components/CreateTemplateModal.jsx
  17. 6 6
      src/client/js/components/EmptyTrashModal.jsx
  18. 13 2
      src/client/js/components/FormattedDistanceDate.jsx
  19. 23 7
      src/client/js/components/Page/PageManagement.jsx
  20. 29 7
      src/client/js/components/Page/TrashPageAlert.jsx
  21. 157 0
      src/client/js/components/PageDeleteModal.jsx
  22. 4 3
      src/client/js/components/PageManagement/ApiErrorMessage.jsx
  23. 5 6
      src/client/js/components/Sidebar.jsx
  24. 10 6
      src/client/js/components/Sidebar/CustomSidebar.jsx
  25. 7 6
      src/client/js/components/Sidebar/RecentChanges.jsx
  26. 37 22
      src/client/js/components/Sidebar/SidebarNav.jsx
  27. 0 50
      src/client/js/legacy/crowi.js
  28. 43 0
      src/client/js/services/PageContainer.js
  29. 0 19
      src/client/styles/scss/_layout.scss
  30. 10 0
      src/client/styles/scss/_navbar.scss
  31. 4 0
      src/client/styles/scss/_search.scss
  32. 71 14
      src/client/styles/scss/_sidebar.scss
  33. 1 1
      src/client/styles/scss/_variables.scss
  34. 1 423
      src/client/styles/scss/theme/_reboot-bootstrap-colors.scss
  35. 2 0
      src/client/styles/scss/theme/_reboot-bootstrap-theme-colors.scss
  36. 2 2
      src/server/views/admin/app.html
  37. 2 2
      src/server/views/admin/global-notification-detail.html
  38. 1 1
      src/server/views/admin/markdown.html
  39. 2 2
      src/server/views/admin/notification.html
  40. 2 2
      src/server/views/admin/search.html
  41. 3 3
      src/server/views/layout/layout.html
  42. 1 50
      src/server/views/modal/create_template.html
  43. 0 74
      src/server/views/modal/delete.html
  44. 5 5
      src/server/views/modal/empty_trash.html
  45. 1 6
      src/server/views/widget/page_alerts.html
  46. 1 0
      src/server/views/widget/page_content.html
  47. 16 16
      src/server/views/widget/page_list.html
  48. 0 1
      src/server/views/widget/page_modals.html
  49. 4 4
      yarn.lock

+ 13 - 4
CHANGES.md

@@ -18,11 +18,11 @@
 * Support: Upgrade libs
     * bootstrap
 
-## v3.8.1-RC
+## v3.8.2-RC
 
-* Fix: Unset overflow-y style for Edit Tags Modal
+*
 
-## v3.8.0
+## v3.8.1
 
 ### BREAKING CHANGES
 
@@ -33,11 +33,20 @@ Upgrading Guide: <https://docs.growi.org/en/admin-guide/upgrading/38x.html>
 ### Updates
 
 * Improvement: Change the health check method for Elasticsearch
+* Fix: Unset overflow-y style for Edit Tags Modal
+* Fix: Duplicate page source is overwrited
+    * Introduced by 3.7.6
 
-## v3.7.6
+## v3.8.0  (Missing number)
+
+## v3.7.7
 
 * Feature: Empty trash pages
 * Improvement: Behavior of Reconnect to Elasticsearch button
+* Fix: Duplicate page source is overwrited
+    * Introduced by 3.7.6
+
+## v3.7.6  (Missing number)
 
 ## v3.7.5
 

+ 1 - 1
package.json

@@ -167,7 +167,7 @@
     "babel-loader": "^8.0.6",
     "babel-plugin-lodash": "^3.3.4",
     "babel-plugin-transform-imports": "^2.0.0",
-    "bootstrap": "^4.4.1",
+    "bootstrap": "^4.5.0",
     "browser-bunyan": "^1.3.0",
     "browser-sync": "^2.26.3",
     "bunyan-debug": "^2.0.0",

+ 1 - 1
resource/cdn-manifests.js

@@ -3,7 +3,7 @@ module.exports = {
     {
       name: 'basis',
       // eslint-disable-next-line max-len
-      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.4.1/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
+      url: 'https://cdn.jsdelivr.net/combine/npm/emojione@3.1.2,npm/jquery@3.4.0,npm/popper.js@1.15.0,npm/bootstrap@4.5.0/dist/js/bootstrap.min.js,npm/scrollpos-styler@0.7.1,npm/jquery-slimscroll@1.3.8/jquery.slimscroll.min.js',
       groups: ['basis'],
       args: {
         integrity: '',

+ 112 - 111
resource/locales/en-US/sandbox.md

@@ -1,7 +1,7 @@
 <div class="card">
   <div class="card-body">
 
-# 目次
+# Table of Contents
 
 ```
 @[toc]
@@ -14,9 +14,9 @@
 
 # :pencil: Block Elements
 
-## Headers 見出し
+## Headers
 
-先頭に`#`をレベルの数だけ記述します。
+Add one `#` per level at the start of the line
 
 ```
 # Header 1
@@ -35,28 +35,29 @@
 
 ###### Header 6
 
-## Block 段落
+## Block paragraph
 
-空白行を挟むことで段落となります。aaaa
+Pararaphs are created by inserting a newline character
+A paragraph can be created by pressing Enter at the end of the previous paragraph.
 
 ```
-段落1
-(空行)
-段落2
+paragraph1
+(Blank line)
+paragraph2
 ```
 
-段落1
+paragraph1
 
-段落2
+paragraph2
 
-## Br 改行
+## Br new line
 
-改行の前に半角スペース``を2つ記述します。
-***この挙動は、オプションで変更可能です***
+Add two spaces before break.
+***This behaviour can be modified in the options menu.***
 
 ```
 hoge
-fuga(スペース2つ)
+fuga(two spaces)
 piyo
 ```
 
@@ -64,31 +65,31 @@ hoge
 fuga
 piyo
 
-## Blockquotes 引用
+## Blockquotes
 
-先頭に`>`を記述します。ネストは`>`を多重に記述します。
+Add one `>` per level at the start of the line
 
 ```
-> 引用
-> 引用
->> 多重引用
+> quote
+> quote
+>> nested quotes
 ```
 
-> 引用
-> 引用
->> 多重引用
+> quote
+> quote
+>> nested quotes
 
-## Code コード
+## Code
 
-`` `バッククオート` `` 3つ、あるいはチルダ`~`3つで囲みます。
+Wrap code with three back quotes or tildes.
 
 ```
 print 'hoge'
 ```
 
-### シンタックスハイライトとファイル名
+### Syntax highlight and file name
 
-- [highlight.js Demo](https://highlightjs.org/static/demo/) の common カテゴリ内の言語に対応しています
+- corresponding [highlight.js Demo](https://highlightjs.org/static/demo/) of common category
 
 
 ~~~
@@ -115,19 +116,19 @@ function MersenneTwister(seed) {
 }
 ```
 
-### インラインコード
+### Inline code
 
-`` `バッククオート` `` で単語を囲むとインラインコードになります。
+Words wrapped by `` `back quotes` `` will be formatted as inline code.
 
 ```
-これは `インラインコード`です。
+This is `Inline Code`.
 ```
 
-これは `インラインコード`です。
+This is  `Inline Code`.
 
-## pre 整形済みテキスト
+## Pre-arranged text
 
-半角スペース4個もしくはタブで、コードブロックをpre表示できます
+Code blocks should be preceded by four spaces or one tab.
 
 ```
     class Hoge
@@ -143,9 +144,9 @@ function MersenneTwister(seed) {
         end
     end
 
-## Hr 水平線
+## Horizontal Line
 
-アンダースコア`_` 、アスタリスク`*`を3つ以上連続して記述します。
+Write three underscores `_`, or asterisks`*`.
 
 ```
 ***
@@ -161,47 +162,47 @@ ___
 
 # :pencil: Typography
 
-## 強調
+## Strong Text
 
-### em
+### Italic
 
-アスタリスク`*`もしくはアンダースコア`_`1個で文字列を囲みます。
+To italicize text, add One asterisk or underscores before and after a word or phrase.
 
 ```
-これは *イタリック* です
-これは _イタリック_ です
+This is *Italic* .
+This is _Italic_ .
 ```
 
-これは *イタリック* です
-これは _イタリック_ です
+This is *Italic* .
+This is _Italic_ .
 
-### strong
+### Bold
 
-アスタリスク`*`もしくはアンダースコア`_`2個で文字列を囲みます。
+To bold text, add two asterisks or underscores before and after a word or phrase.
 
 ```
-これは **ボールド** です
-これは __ボールド__ です
+This is **bold**.
+This is __bold__.
 ```
 
-これは **ボールド** です
-これは __ボールド__ です
+This is **bold**.
+This is __bold__.
 
-### em + strong
+### Bold + Italic
 
-アスタリスク`*`もしくはアンダースコア`_`3個で文字列を囲みます。
+To bold and italicize text, add three asterisks or underscores before and after a word or phrase.
 
 ```
-これは ***イタリック&ボールド*** です
-これは ___イタリック&ボールド___ です
+This is ***Italic & Bold***.
+This is ___Italic & Bold___.
 ```
 
-これは ***イタリック&ボールド*** です
-これは ___イタリック&ボールド___ です
+This is ***Italic & Bold***.
+This is ___Italic & Bold___.
 
 # :pencil: Images
 
-`![Alt文字列](URL)` で`<img>`タグを挿入できます。
+You can insert `<img>` tag using `![description](URL)`.
 
 ```markdown
 ![Minion](https://octodex.github.com/images/minion.png)
@@ -211,7 +212,7 @@ ___
 ![Minion](https://octodex.github.com/images/minion.png)
 ![Stormtroopocat](https://octodex.github.com/images/stormtroopocat.jpg "The Stormtroopocat")
 
-画像の大きさなどの指定をする場合はimgタグを使用します。
+The size of the image can be set by using an HTML image tag
 
 ```html
 <img src="https://octodex.github.com/images/dojocat.jpg" width="200px">
@@ -222,9 +223,9 @@ ___
 
 # :pencil: Link
 
-## Markdown 標準
+## Markdown standard
 
-`[表示テキスト](URL)`でリンクに変換されます。
+You can create links using `[Display text](URL)`.
 
 ```
 [Google](https://www.google.co.jp/)
@@ -232,7 +233,7 @@ ___
 
 [Google](https://www.google.co.jp/)
 
-## Crowi 互換
+## Crowi compatibility
 
 ```
 [/Sandbox]
@@ -247,80 +248,80 @@ ___
 (available by [weseek/growi-plugin-pukiwiki-like-linker
 ](https://github.com/weseek/growi-plugin-pukiwiki-like-linker) )
 
-最も柔軟な Linker です。
-記述中のページを基点とした相対リンクと、表示テキストに対するリンクを同時に実現できます。
+This is the most flexible linker.
+Both the page description and link address can be displayed on the page.
 
 ```
 [[./Bootstrap3]]
-Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+Example of Bootstrap3 is[[here>./Bootstrap3]]
 ```
 
 [[../user]]
-Bootstrap3のExampleは[[こちら>./Bootstrap3]]
+Example of Bootstrap3 is[[here>./Bootstrap3]]
 
 # :pencil: Lists
 
-## Ul 箇条書きリスト
+## Ul Bulleted list
 
-ハイフン`-`、プラス`+`、アスタリスク`*`のいずれかを先頭に記述します。
-ネストはタブで表現します。
+To create an unordered list, add dashes (-), asterisks (*), or plus signs (+) in front of line items. 
+Items can be nested using indentation.
 
 ```
-- リスト1
-    - リスト1_1
-        - リスト1_1_1
-        - リスト1_1_2
-    - リスト1_2
-- リスト2
-- リスト3
+- List1
+    - List1_1
+        - List1_1_1
+        - List1_1_2
+    - List1_2
+- List2
+- List3
 ```
 
-- リスト1
-    - リスト1_1
-        - リスト1_1_1
-        - リスト1_1_2
-    - リスト1_2
-- リスト2
-- リスト3
+- List1
+    - List1_1
+        - List1_1_1
+        - List1_1_2
+    - List1_2
+- List2
+- List3
 
-## Ol 番号付きリスト
+## Ol Numbered List
 
-`番号.`を先頭に記述します。ネストはタブで表現します。
-番号は自動的に採番されるため、すべての行を1.と記述するのがお勧めです。
+To create an ordered list, add line items with numbers followed by periods. 
+The numbers don’t have to be in numerical order, but the list should start with the number one.
 
 ```
-1. 番号付きリスト1
-    1. 番号付きリスト1-1
-    1. 番号付きリスト1-2
-1. 番号付きリスト2
-1. 番号付きリスト3
+1. Number list 1
+    1. Number list 1-1
+    1. Number list 1-2
+1. Number list 2
+1. Number list 3
 ```
 
-1. 番号付きリスト1
-    1. 番号付きリスト1-1
-    1. 番号付きリスト1-2
-1. 番号付きリスト2
-1. 番号付きリスト3
+1. Number list 1
+    1. Number list 1-1
+    1. Number list 1-2
+1. Number list 2
+1. Number list 3
 
 
-## タスクリスト
+## Check list
 
 ```
-- [ ] タスク 1
-    - [x] タスク 1.1
-    - [ ] タスク 1.2
-- [x] タスク2
+- [ ] Task 1
+    - [x] Task 1.1
+    - [ ] Task 1.2
+- [x] Task2
 ```
 
-- [ ] タスク 1
-    - [x] タスク 1.1
-    - [ ] タスク 1.2
-- [x] タスク2
+- [ ] Task 1
+    - [x] Task 1.1
+    - [ ] Task 1.2
+- [x] Task2
 
 
 # :pencil: Table
 
-## Markdown 標準
+## Markdown Standard
 
 ```markdown
 | Left align | Right align | Center align |
@@ -353,7 +354,7 @@ aligned    | aligned     | aligned
 | left       | right       | center       |
 | aligned    | aligned     | aligned      |
 
-## TSV (crowi-plus 独自記法)
+## TSV (crowi-plus notation)
 
 ```
 ::: tsv
@@ -367,7 +368,7 @@ Content Cell Content Cell
 Content Cell Content Cell
 :::
 
-## TSV ヘッダ付き (crowi-plus 独自記法)
+## TSV with header (crowi-plus notation)
 
 ```
 ::: tsv-h
@@ -383,7 +384,7 @@ Content Cell Content Cell
 Content Cell Content Cell
 :::
 
-## CSV (crowi-plus 独自記法)
+## CSV (crowi-plus original notation)
 
 ```
 ::: csv
@@ -397,7 +398,7 @@ Content Cell,Content Cell
 Content Cell,Content Cell
 :::
 
-## CSV ヘッダ付き (crowi-plus 独自記法)
+## CSV with header (crowi-plus original notation)
 
 ```
 ::: csv-h
@@ -416,15 +417,15 @@ Content Cell,Content Cell
 
 # :pencil: Footnote
 
-脚注への参照[^1]を書くことができます。また、インラインの脚注^[インラインで記述できる脚注です]を入れる事も出来ます。
+You can write a reference [^1] to a footnote. You can also add an inline footnote^[Inline_footnote].
 
-長い脚注は[^longnote]のように書くことができます。
+Long footnotes can be written as [^longnote].
 
-[^1]: 1つめの脚注への参照です。
+[^1]: A_reference_to_the_first_footnote.
 
-[^longnote]: 脚注を複数ブロックで書く例です。
+[^longnote]: An_example_of_writing_a_footnote_in_multiple_blocks.
 
-    後続の段落はインデントされて、前の脚注に属します。
+    Subsequent paragraphs are indented and belong to the previous footnote.
 
 
 # :pencil: Emoji

+ 4 - 2
resource/locales/en-US/translation.json

@@ -130,6 +130,8 @@
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
   "Disassociate": "Disassociate",
+  "Recent Created": "Recent Created",
+  "Recent Changes": "Recent Changes",
   "form_validation": {
     "error_message": "Some values ​​are incorrect",
     "required": "%s is required",
@@ -281,7 +283,7 @@
     "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":{
@@ -338,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 - 2
resource/locales/ja/translation.json

@@ -129,6 +129,8 @@
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
   "Disassociate": "連携解除",
+  "Recent Created": "最新の作成",
+  "Recent Changes": "最新の変更",
   "form_validation": {
     "error_message": "いくつかの値が設定されていません",
     "required": "%sに値を入力してください",
@@ -279,7 +281,7 @@
     "delete_recursively": "全ての子ページも削除",
     "delete_completely": "完全削除",
     "delete_completely_restriction": "完全削除をするための権限がありません。",
-    "recursively": "<code>%s</code> 配下のページも削除します",
+    "recursively": "配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   "modal_empty":{
@@ -336,7 +338,7 @@
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "以下のパスにテンプレートページを作成"
+      "Create template under": "配下にテンプレートページを作成"
     },
     "option_label": {
       "select": "テンプレートタイプを選択してください",

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

@@ -13,6 +13,7 @@ import AppContainer from './services/AppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import PageCreateButton from './components/Navbar/PageCreateButton';
 import PageCreateModal from './components/PageCreateModal';
+import CreateTemplateModal from './components/CreateTemplateModal';
 import RenameModal from './components/RenameModal';
 
 const logger = loggerFactory('growi:app');
@@ -50,6 +51,7 @@ const componentMappings = {
   'create-page-button': <PageCreateButton />,
   'create-page-button-icon': <PageCreateButton isIcon />,
   'page-create-modal': <PageCreateModal />,
+  'create-template-modal': <CreateTemplateModal />,
   'rename-modal': <RenameModal />,
 
   'grw-sidebar-wrapper': <Sidebar />,

+ 2 - 2
src/client/js/components/Admin/Common/AdminNavigation.jsx

@@ -19,7 +19,7 @@ const AdminNavigation = (props) => {
           href="/admin"
           className={`${pageTransitionClassName} ${pathname === '/admin' && 'active'}`}
         >
-          <i className="icon-fw icon-home"></i> {t('Management Wiki Home')}
+          <i className="icon-fw icon-home"></i> {t('Wiki Management Home Page')}
         </a>
         <a
           href="/admin/app"
@@ -100,7 +100,7 @@ const AdminNavigation = (props) => {
           aria-expanded="false"
         >
           <span className="float-left"><i className="icon-fw icon-home"></i>
-            {pathname === '/admin' && t('Management Wiki Home')}
+            {pathname === '/admin' && t('Wiki Management Home Page')}
             {pathname === '/admin/app' && t('App Settings')}
             {pathname === '/admin/security' && t('security_settings')}
             {pathname === '/admin/markdown' && t('Markdown Settings')}

+ 1 - 1
src/client/js/components/Admin/Security/GitHubSecuritySetting.jsx

@@ -102,7 +102,7 @@ class GitHubSecurityManagement extends React.Component {
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}

+ 1 - 1
src/client/js/components/Admin/Security/GoogleSecuritySetting.jsx

@@ -102,7 +102,7 @@ class GoogleSecurityManagement extends React.Component {
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}

+ 2 - 2
src/client/js/components/Admin/Security/OidcSecuritySetting.jsx

@@ -97,7 +97,7 @@ class OidcSecurityManagement extends React.Component {
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}
@@ -253,7 +253,7 @@ class OidcSecurityManagement extends React.Component {
                     <i
                       className="icon-exclamation"
                       // eslint-disable-next-line max-len
-                      dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                      dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                     />
                   </div>
                 )}

+ 1 - 1
src/client/js/components/Admin/Security/SamlSecuritySetting.jsx

@@ -124,7 +124,7 @@ class SamlSecurityManagement extends React.Component {
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}

+ 1 - 1
src/client/js/components/Admin/Security/TwitterSecuritySetting.jsx

@@ -102,7 +102,7 @@ class TwitterSecurityManagement extends React.Component {
                 <i
                   className="icon-exclamation"
                   // eslint-disable-next-line max-len
-                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App settings')}<i class="icon-login"></i></a>` }) }}
+                  dangerouslySetInnerHTML={{ __html: t('security_setting.alert_siteUrl_is_not_set', { link: `<a href="/admin/app">${t('App Settings')}<i class="icon-login"></i></a>` }) }}
                 />
               </div>
             )}

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

@@ -174,11 +174,11 @@ class UserManagement extends React.Component {
 
             <div className="col-md-6 my-2">
               <div className="form-inline">
-                {this.renderCheckbox('all', 'All', 'primary')}
+                {this.renderCheckbox('all', 'All', 'secondary')}
                 {this.renderCheckbox('registered', 'Approval Pending', 'info')}
                 {this.renderCheckbox('active', 'Active', 'success')}
                 {this.renderCheckbox('suspended', 'Suspended', 'warning')}
-                {this.renderCheckbox('invited', 'Invited', 'info')}
+                {this.renderCheckbox('invited', 'Invited', 'pink')}
               </div>
               <div>
                 {

+ 2 - 2
src/client/js/components/Admin/Users/UserTable.jsx

@@ -50,7 +50,7 @@ class UserTable extends React.Component {
         text = 'Deleted';
         break;
       case 5:
-        additionalClassName = 'badge-info';
+        additionalClassName = 'badge-pink';
         text = 'Invited';
         break;
     }
@@ -71,7 +71,7 @@ class UserTable extends React.Component {
     const { t } = this.props;
 
     if (isAdmin) {
-      return <span className="badge badge-primary badge-pill ml-2">{t('admin:user_management.user_table.administrator')}</span>;
+      return <span className="badge badge-indigo badge-pill ml-2">{t('admin:user_management.user_table.administrator')}</span>;
     }
   }
 

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

@@ -0,0 +1,87 @@
+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, isCreateTemplatePageModalShown } = 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={isCreateTemplatePageModalShown} toggle={pageContainer.closeCreateTemplatePageModal} className="grw-create-page">
+      <ModalHeader tag="h4" toggle={pageContainer.closeCreateTemplatePageModal} 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,
+};
+
+export default withTranslation()(CreateTemplateModalWrapper);

+ 6 - 6
src/client/js/components/EmptyTrashModal.jsx

@@ -9,12 +9,12 @@ import { withTranslation } from 'react-i18next';
 
 const EmptyTrashModal = (props) => {
   const {
-    t, isOpen, toggle, onClickSubmit,
+    t, isOpen, onClose, onClickEmptyBtn,
   } = props;
 
   return (
-    <Modal isOpen={isOpen} toggle={toggle} className="grw-create-page">
-      <ModalHeader tag="h4" toggle={toggle} className="bg-danger text-light">
+    <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>
@@ -22,7 +22,7 @@ const EmptyTrashModal = (props) => {
       </ModalBody>
       <ModalFooter>
         {/* TODO add error message */}
-        <button type="button" className="btn btn-danger" onClick={onClickSubmit}>
+        <button type="button" className="btn btn-danger" onClick={onClickEmptyBtn}>
           <i className="icon-trash mr-2" aria-hidden="true"></i>Empty
         </button>
       </ModalFooter>
@@ -35,8 +35,8 @@ EmptyTrashModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
 
   isOpen: PropTypes.bool.isRequired,
-  toggle: PropTypes.func.isRequired,
-  onClickSubmit: PropTypes.func.isRequired,
+  onClose: PropTypes.func.isRequired,
+  onClickEmptyBtn: PropTypes.func.isRequired,
 };
 
 export default withTranslation()(EmptyTrashModal);

+ 13 - 2
src/client/js/components/FormattedDistanceDate.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import PropTypes from 'prop-types';
 
-import { format, formatDistanceStrict } from 'date-fns';
+import { format, formatDistanceStrict, differenceInSeconds } from 'date-fns';
 import { UncontrolledTooltip } from 'reactstrap';
 
 const FormattedDistanceDate = (props) => {
@@ -11,9 +11,15 @@ const FormattedDistanceDate = (props) => {
 
   const baseDate = props.baseDate || new Date();
 
-  const elemId = `grw-fdd-${props.id}`;
   const dateFormatted = format(date, 'yyyy/MM/dd HH:mm');
 
+  const diff = Math.abs(differenceInSeconds(date, baseDate));
+  if (diff > props.differenceForAvoidingFormat) {
+    return <>{dateFormatted}</>;
+  }
+
+  const elemId = `grw-fdd-${props.id}`;
+
   return (
     <>
       <span id={elemId}>{formatDistanceStrict(date, baseDate)}</span>
@@ -26,6 +32,11 @@ FormattedDistanceDate.propTypes = {
   id: PropTypes.string.isRequired,
   date: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired,
   baseDate: PropTypes.instanceOf(Date),
+  // the number(sec) from 'baseDate' to avoid format
+  differenceForAvoidingFormat: PropTypes.number,
+};
+FormattedDistanceDate.defaultProps = {
+  differenceForAvoidingFormat: 86400 * 3,
 };
 
 export default FormattedDistanceDate;

+ 23 - 7
src/client/js/components/Page/PageManagement.jsx

@@ -1,19 +1,29 @@
-import React from 'react';
+import React, { useState } from 'react';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 
-import { isTopPage, isUserPage } from '@commons/util/path-utils';
+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';
 
 
 const PageManagement = (props) => {
   const { t, appContainer, pageContainer } = props;
-  const { path } = pageContainer.state;
+  const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
   const { currentUser } = appContainer;
   const isTopPagePath = isTopPage(path);
-  const isUserPagePath = isUserPage(path);
+
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
+
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function closePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
 
   function renderDropdownItemForNotTopPage() {
     return (
@@ -33,7 +43,7 @@ const PageManagement = (props) => {
     return (
       <>
         <div className="dropdown-divider"></div>
-        <a className="dropdown-item" href="#" data-target="#deletePage" data-toggle="modal">
+        <a className="dropdown-item" type="button" onClick={openPageDeleteModalHandler}>
           <i className="icon-fw icon-fire text-danger"></i> { t('Delete') }
         </a>
       </>
@@ -55,11 +65,17 @@ const PageManagement = (props) => {
       </a>
       <div className="dropdown-menu dropdown-menu-right">
         {!isTopPagePath && renderDropdownItemForNotTopPage()}
-        <a className="dropdown-item" href="#" data-target="#create-template" data-toggle="modal">
+        <a className="dropdown-item" onClick={pageContainer.openCreateTemplatePageModal}>
           <i className="icon-fw icon-magic-wand"></i> { t('template.option_label.create/edit') }
         </a>
-        {(!isTopPagePath && !isUserPagePath) && renderDropdownItemForDeletablePage()}
+        {(!isTopPagePath && isDeletable) && renderDropdownItemForDeletablePage()}
       </div>
+      <PageDeleteModal
+        isOpen={isPageDeleteModalShown}
+        onClose={closePageDeleteModalHandler}
+        path={path}
+        isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+      />
     </>
   );
 };

+ 29 - 7
src/client/js/components/Page/TrashPageAlert.jsx

@@ -8,7 +8,9 @@ import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import UserPicture from '../User/UserPicture';
+
 import EmptyTrashModal from '../EmptyTrashModal';
+import PageDeleteModal from '../PageDeleteModal';
 
 
 const TrashPageAlert = (props) => {
@@ -18,16 +20,25 @@ const TrashPageAlert = (props) => {
   } = pageContainer.state;
   const { currentUser } = appContainer;
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
+  const [isPageDeleteModalShown, setIsPageDeleteModalShown] = useState(false);
 
-  function openEmptyTrashModal() {
+  function openEmptyTrashModalHandler() {
     setIsEmptyTrashModalShown(true);
   }
 
-  function closeEmptyTrashModal() {
+  function closeEmptyTrashModalHandler() {
     setIsEmptyTrashModalShown(false);
   }
 
-  async function onClickDeleteBtn() {
+  function openPageDeleteModalHandler() {
+    setIsPageDeleteModalShown(true);
+  }
+
+  function opclosePageDeleteModalHandler() {
+    setIsPageDeleteModalShown(false);
+  }
+
+  async function emptyTrash() {
     try {
       await appContainer.apiv3Delete('/pages/empty-trash');
       window.location.reload();
@@ -37,6 +48,11 @@ const TrashPageAlert = (props) => {
     }
   }
 
+  function emptyButtonHandler() {
+    emptyTrash();
+  }
+
+
   function renderEmptyButton() {
     return (
       <button
@@ -44,7 +60,7 @@ const TrashPageAlert = (props) => {
         type="button"
         className="btn btn-danger rounded-pill btn-sm ml-auto"
         data-target="#emptyTrash"
-        onClick={openEmptyTrashModal}
+        onClick={openEmptyTrashModalHandler}
       >
         <i className="icon-trash" aria-hidden="true"></i>{ t('modal_empty.empty_the_trash') }
       </button>
@@ -66,8 +82,7 @@ const TrashPageAlert = (props) => {
           type="button"
           className="btn btn-danger rounded-pill btn-sm mr-2"
           disabled={!isAbleToDeleteCompletely}
-          data-target="#deletePage"
-          data-toggle="modal"
+          onClick={openPageDeleteModalHandler}
         >
           <i className="icon-fire" aria-hidden="true"></i> { t('Delete Completely') }
         </button>
@@ -85,7 +100,14 @@ const TrashPageAlert = (props) => {
         {(currentUser.admin && path === '/trash' && hasChildren) && renderEmptyButton()}
         {(isDeleted && currentUser != null) && renderTrashPageManagementButtons()}
       </div>
-      <EmptyTrashModal isOpen={isEmptyTrashModalShown} toggle={closeEmptyTrashModal} onClickSubmit={onClickDeleteBtn} />
+      <EmptyTrashModal isOpen={isEmptyTrashModalShown} onClose={closeEmptyTrashModalHandler} onClickEmptyBtn={emptyButtonHandler} />
+      <PageDeleteModal
+        isOpen={isPageDeleteModalShown}
+        onClose={opclosePageDeleteModalHandler}
+        path={path}
+        isDeleteCompletelyModal
+        isAbleToDeleteCompletely={isAbleToDeleteCompletely}
+      />
     </>
   );
 };

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

@@ -0,0 +1,157 @@
+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);

+ 4 - 3
src/client/js/components/PageManagement/ApiErrorMessage.jsx

@@ -17,6 +17,10 @@ const ApiErrorMessage = (props) => {
             <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>
+        );
       default:
         return null;
     }
@@ -43,9 +47,6 @@ const ApiErrorMessage = (props) => {
 
   // TODO GW-79 Set according to error message
   // <div>
-  //   <span className="text-danger msg msg-notfound_or_forbidden">
-  //     <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.notfound_or_forbidden') }</strong>
-  //   </span>
   //   <span className="text-danger msg msg-user_not_admin">
   //     <strong><i className="icon-fw icon-ban"></i>{ t('page_api_error.user_not_admin') }</strong>
   //   </span>

+ 5 - 6
src/client/js/components/Sidebar.jsx

@@ -14,7 +14,7 @@ import { createSubscribedElement } from './UnstatedUtils';
 import AppContainer from '../services/AppContainer';
 
 import SidebarNav from './Sidebar/SidebarNav';
-import History from './Sidebar/History';
+import RecentChanges from './Sidebar/RecentChanges';
 import CustomSidebar from './Sidebar/CustomSidebar';
 
 
@@ -28,7 +28,7 @@ class Sidebar extends React.Component {
   };
 
   state = {
-    currentContentsId: 'custom',
+    currentContentsId: 'recent',
   };
 
   componentWillMount() {
@@ -103,11 +103,11 @@ class Sidebar extends React.Component {
   );
 
   renderSidebarContents = () => {
-    let contents = <CustomSidebar></CustomSidebar>;
+    let contents = <CustomSidebar />;
 
     switch (this.state.currentContentsId) {
-      case 'history':
-        contents = <History></History>;
+      case 'recent':
+        contents = <RecentChanges />;
         break;
     }
 
@@ -136,7 +136,6 @@ class Sidebar extends React.Component {
               // experimental_fullWidthFlyout
               shouldHideGlobalNavShadow
               showContextualNavigation
-              topOffset={50}
             >
             </LayoutManager>
           </ThemeProvider>

+ 10 - 6
src/client/js/components/Sidebar/CustomSidebar.jsx

@@ -25,23 +25,27 @@ class CustomSidebar extends React.Component {
 
   render() {
     return (
-      <>
+      <div className="grw-sidebar-custom">
         <HeaderSection>
           { () => (
-            <div className="grw-sidebar-header-container">
-              {this.renderHeaderWordmark()}
+            <div className="grw-sidebar-header-container p-3 d-flex">
+              <h3>Custom Sidebar</h3>
+              <button type="button" className="btn btn-xs btn-outline-secondary ml-auto" onClick={this.reloadData}>
+                <i className="icon icon-reload"></i>
+              </button>
             </div>
           ) }
         </HeaderSection>
         <MenuSection>
           { () => (
-            <div className="grw-sidebar-content-container">
-              <span>(TBD) CustomSidebar Contents</span>
+            <div className="grw-sidebar-content-container p-3">
+              (TBD) Under implementation
             </div>
           ) }
         </MenuSection>
-      </>
+      </div>
     );
+
   }
 
 }

+ 7 - 6
src/client/js/components/Sidebar/History.jsx → src/client/js/components/Sidebar/RecentChanges.jsx

@@ -22,7 +22,7 @@ import FormattedDistanceDate from '../FormattedDistanceDate';
 import UserPicture from '../User/UserPicture';
 
 const logger = loggerFactory('growi:History');
-class History extends React.Component {
+class RecentChanges extends React.Component {
 
   static propTypes = {
     t: PropTypes.func.isRequired, // i18next
@@ -89,8 +89,9 @@ class History extends React.Component {
         <HeaderSection>
           { () => (
             <div className="grw-sidebar-header-container p-3 d-flex">
-              <h3>{t('History')}</h3>
-              <button type="button" className="btn xs btn-secondary ml-auto" onClick={this.reloadData}>
+              <h3>{t('Recent Changes')}</h3>
+              {/* <h3>{t('Recent Created')}</h3> */} {/* TODO: impl switching */}
+              <button type="button" className="btn btn-xs btn-outline-secondary ml-auto" onClick={this.reloadData}>
                 <i className="icon icon-reload"></i>
               </button>
             </div>
@@ -114,8 +115,8 @@ class History extends React.Component {
 /**
  * Wrapper component for using unstated
  */
-const HistoryWrapper = (props) => {
-  return createSubscribedElement(History, props, [AppContainer]);
+const RecentChangesWrapper = (props) => {
+  return createSubscribedElement(RecentChanges, props, [AppContainer]);
 };
 
-export default withTranslation()(HistoryWrapper);
+export default withTranslation()(RecentChangesWrapper);

+ 37 - 22
src/client/js/components/Sidebar/SidebarNav.jsx

@@ -5,7 +5,6 @@ import { withTranslation } from 'react-i18next';
 
 import {
   GlobalNav,
-  GlobalItem,
 } from '@atlaskit/navigation-next';
 
 import { createSubscribedElement } from '../UnstatedUtils';
@@ -29,34 +28,32 @@ class SidebarNav extends React.Component {
     }
   }
 
-  generatePrimaryItemObj(id, label, icon) {
+  generatePrimaryItemObj(id, label, iconClassNames) {
     const isSelected = this.props.currentContentsId === id;
 
     return {
       id,
       component: ({ className }) => (
         <div className={`${className} grw-global-item-container ${isSelected ? 'active' : ''}`}>
-          <GlobalItem
-            icon={icon}
-            label={label}
-            isSelected={isSelected}
+          <button
+            type="button"
+            className="btn btn-primary btn-lg"
             onClick={() => this.itemSelectedHandler(id)}
-          />
+          >
+            <i className={iconClassNames}></i>
+          </button>
         </div>
       ),
     };
   }
 
-  generateSecondaryItemObj(id, label, icon, href) {
+  generateSecondaryItemObj(id, label, iconClassNames, href, isBlank, isHiddenOnLargeDevice) {
     return {
       id,
       component: ({ className }) => (
-        <div className={`${className} grw-global-item-container d-block d-md-none`}>
-          <a href={href}>
-            <GlobalItem
-              icon={icon}
-              label={label}
-            />
+        <div className={`${className} grw-global-item-container ${isHiddenOnLargeDevice ? 'd-block d-md-none' : ''}`}>
+          <a href={href} className="btn btn-primary btn-lg" target={`${isBlank ? '_blank' : ''}`}>
+            <i className={iconClassNames}></i>
           </a>
         </div>
       ),
@@ -68,22 +65,40 @@ class SidebarNav extends React.Component {
   }
 
   render() {
+    const { isAdmin, currentUsername } = this.props.appContainer;
+
+    const primaryItems = [
+      this.generatePrimaryItemObj('custom', 'Custom Sidebar', 'fa fa-code'),
+      this.generatePrimaryItemObj('recent', 'Recent Changes', 'icon-clock'),
+      // this.generatePrimaryItemObj('tag', 'Tags', 'icon-tag'),
+      // this.generatePrimaryItemObj('favorite', 'Favorite', 'icon-star'),
+    ];
+
+    const secondaryItems = [
+      this.generateSecondaryItemObj('draft', 'Draft', 'icon-docs', `/user/${currentUsername}#user-draft-list`),
+      this.generateSecondaryItemObj('help', 'Help', 'icon-question', 'https://docs.growi.org', true),
+      this.generateSecondaryItemObj('trash', 'Trash', 'icon-trash', '/trash'),
+    ];
+    if (isAdmin) {
+      secondaryItems.unshift( // add to the beginning
+        this.generateSecondaryItemObj('admin', 'Admin', 'icon-settings', '/admin', false, true),
+      );
+    }
+
     return (
       <GlobalNav
-        primaryItems={[
-          this.generatePrimaryItemObj('custom', 'Custom Sidebar', this.generateIconFactory('fa fa-code')),
-          this.generatePrimaryItemObj('history', 'History', this.generateIconFactory('icon-clock')),
-        ]}
-        secondaryItems={[
-          this.generateSecondaryItemObj('admin', 'Admin', this.generateIconFactory('icon-settings'), '/admin'),
-          this.generateSecondaryItemObj('help', 'Help', this.generateIconFactory('icon-question'), 'https://docs.growi.org'),
-        ]}
+        primaryItems={primaryItems}
+        secondaryItems={secondaryItems}
       />
     );
   }
 
 }
 
+SidebarNav.propTypes = {
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+};
+
 /**
  * Wrapper component for using unstated
  */

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

@@ -323,37 +323,6 @@ $(() => {
 
     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) => {
@@ -379,25 +348,6 @@ $(() => {
 
     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');

+ 43 - 0
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;
@@ -48,6 +49,7 @@ export default class PageContainer extends Container {
       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')),
@@ -62,6 +64,7 @@ export default class PageContainer extends Container {
       isHackmdDraftUpdatingInRealtime: false,
 
       isPageDuplicateModalShown: false,
+      isCreateTemplatePageModalShown: false,
       isRenameRecursively: true,
       isRenameRedirect: false,
       isRenameMetadata: false,
@@ -92,8 +95,23 @@ export default class PageContainer extends Container {
       });
     });
 
+    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);
+        }
+      });
+    }
+
     this.openPageDuplicateModal = this.openPageDuplicateModal.bind(this);
     this.closePageDuplicateModal = this.closePageDuplicateModal.bind(this);
+    this.openCreateTemplatePageModal = this.openCreateTemplatePageModal.bind(this);
+    this.closeCreateTemplatePageModal = this.closeCreateTemplatePageModal.bind(this);
   }
 
   /**
@@ -303,6 +321,23 @@ 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(),
+    });
+
+  }
+
   showSuccessToastr() {
     toastr.success(undefined, 'Saved successfully', {
       closeButton: true,
@@ -403,4 +438,12 @@ export default class PageContainer extends Container {
     this.setState({ isPageDuplicateModalShown: false });
   }
 
+  openCreateTemplatePageModal() {
+    this.setState({ isCreateTemplatePageModalShown: true });
+  }
+
+  closeCreateTemplatePageModal() {
+    this.setState({ isCreateTemplatePageModalShown: false });
+  }
+
 }

+ 0 - 19
src/client/styles/scss/_layout.scss

@@ -1,24 +1,5 @@
 @import 'layout_variable';
 
-%fukidashi-for-active {
-  position: relative;
-
-  // speech balloon
-  &:after {
-    position: absolute;
-    top: 0.5em;
-    right: -1em;
-    display: block;
-    width: 0;
-    content: '';
-    border: 1em solid transparent;
-    border-left-width: 0;
-
-    // @include media-breakpoint-down(xs) {
-    // }
-  }
-}
-
 // FIXME: replace with mt-2 or mt-3
 .grw-mt-10px {
   margin-top: 10px !important;

+ 10 - 0
src/client/styles/scss/_navbar.scss

@@ -6,6 +6,16 @@
     font-size: 1.5em;
   }
 
+  .grw-navbar-search {
+    left: $grw-logo-width + 19px;
+
+    @include media-breakpoint-up(lg) {
+      position: absolute;
+      left: 50%;
+      transform: translate(-50%, 0%);
+    }
+  }
+
   .nav-link,
   .nav-item.confidential {
     display: flex;

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

@@ -110,6 +110,10 @@
     &.focus {
       width: 300px;
     }
+
+    @include media-breakpoint-up(xl) {
+      width: 300px;
+    }
   }
 }
 

+ 71 - 14
src/client/styles/scss/_sidebar.scss

@@ -1,4 +1,22 @@
 .grw-sidebar {
+  $sidebar-nav-button-height: 55px;
+
+  %fukidashi-for-active {
+    position: relative;
+
+    // speech balloon
+    &:after {
+      position: absolute;
+      right: -0.1em;
+      display: block;
+      width: 0;
+      content: '';
+      border: 9px solid transparent;
+      border-left-width: 0;
+      transform: translateY(-#{$sidebar-nav-button-height / 2});
+    }
+  }
+
   .ak-navigation-resize-button {
     // locate to the center of screen
     top: calc(50vh - 20px);
@@ -30,32 +48,71 @@
   }
 
   // override @atlaskit/navigation-next styles
+  $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;
+
     // Adjust to be on top of the growi subnavigation
     z-index: $zindex-sticky + 5;
   }
-
-  .grw-global-item-container {
-    i {
-      font-size: 1.5em;
+  div[data-testid='GlobalNavigation'] {
+    > div {
+      height: calc(100vh - #{$navbar-total-height});
+      padding-top: 0;
+    }
+  }
+  div[class$='-Outer'] {
+    width: 0;
+  }
+  div[class$='-PrimaryItemsList'],
+  div[class$='-SecondaryItemsList'] {
+    > div {
+      padding: 0;
     }
 
-    // icon opacity
-    &:not(.active) {
-      i {
-        opacity: 0.4;
+    .grw-global-item-container {
+      width: unset;
+      height: unset;
+      background-color: transparent;
+      border-radius: 0;
+
+      .btn {
+        width: $grw-sidebar-nav-width;
+        border-radius: 0;
       }
-      &:hover,
-      &:focus {
+
+      // icon opacity
+      &:not(.active) {
         i {
-          opacity: 0.7;
+          opacity: 0.4;
+        }
+        &:hover,
+        &:focus {
+          i {
+            opacity: 0.7;
+          }
         }
       }
     }
+  }
+  div[class$='-PrimaryItemsList'] {
+    .grw-global-item-container {
+      .btn-lg {
+        height: $sidebar-nav-button-height;
 
-    &.active {
-      button {
-        @extend %fukidashi-for-active;
+        i {
+          font-size: 1.5em;
+        }
+      }
+
+      &.active {
+        button {
+          @extend %fukidashi-for-active;
+        }
       }
     }
   }

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

@@ -7,7 +7,7 @@ $font-family-monospace-not-strictly: Monaco, Menlo, Consolas, 'Courier New', Mei
 
 //== Layout
 $grw-navbar-height: 52px;
-$grw-navbar-border-width: 3px;
+$grw-navbar-border-width: 3.3333px;
 
 $grw-sidebar-nav-width: 64px; // !!DO NOT CHANGE!! 'margin-left' for '.css-teprsg' is hardcoded
 $grw-sidebar-content-min-width: 240px;

+ 1 - 423
src/client/styles/scss/theme/_reboot-bootstrap-colors.scss

@@ -1,47 +1,12 @@
 //
 //
 // Apply partially
-//   https://github.com/twbs/bootstrap/blob/v4.4.1/scss/_reboot.scss
+//   https://github.com/twbs/bootstrap/blob/v4.5.0/scss/_reboot.scss
 //
 //
 
 // stylelint-disable at-rule-no-vendor-prefix, declaration-no-important, selector-no-qualifying-type, property-no-vendor-prefix
 
-// Reboot
-//
-// Normalization of HTML elements, manually forked from Normalize.css to remove
-// styles targeting irrelevant browsers while applying new styles.
-//
-// Normalize is licensed MIT. https://github.com/necolas/normalize.css
-
-// Document
-//
-// 1. Change from `box-sizing: content-box` so that `width` is not affected by `padding` or `border`.
-// 2. Change the default font family in all browsers.
-// 3. Correct the line height in all browsers.
-// 4. Prevent adjustments of font size after orientation changes in IE on Windows Phone and in iOS.
-// 5. Change the default tap highlight to be completely transparent in iOS.
-
-// *,
-// *::before,
-// *::after {
-//   box-sizing: border-box; // 1
-// }
-
-// html {
-//   font-family: sans-serif; // 2
-//   line-height: 1.15; // 3
-//   -webkit-text-size-adjust: 100%; // 4
-//   -webkit-tap-highlight-color: rgba($black, 0); // 5
-// }
-
-// Shim for "new" HTML5 structural elements to display correctly (IE10, older browsers)
-// TODO: remove in v5
-// stylelint-disable-next-line selector-list-comma-newline-after
-// article, aside, figcaption, figure, footer, header, hgroup, main, nav, section {
-//   display: block;
-// }
-
 // Body
 //
 // 1. Remove the margin in all browsers.
@@ -60,128 +25,6 @@ body {
   background-color: $body-bg; // 2
 }
 
-// Future-proof rule: in browsers that support :focus-visible, suppress the focus outline
-// on elements that programmatically receive focus but wouldn't normally show a visible
-// focus outline. In general, this would mean that the outline is only applied if the
-// interaction that led to the element receiving programmatic focus was a keyboard interaction,
-// or the browser has somehow determined that the user is primarily a keyboard user and/or
-// wants focus outlines to always be presented.
-//
-// See https://developer.mozilla.org/en-US/docs/Web/CSS/:focus-visible
-// and https://developer.paciellogroup.com/blog/2018/03/focus-visible-and-backwards-compatibility/
-// [tabindex="-1"]:focus:not(:focus-visible) {
-//   outline: 0 !important;
-// }
-
-// Content grouping
-//
-// 1. Add the correct box sizing in Firefox.
-// 2. Show the overflow in Edge and IE.
-
-// hr {
-//   box-sizing: content-box; // 1
-//   height: 0; // 1
-//   overflow: visible; // 2
-// }
-
-//
-// Typography
-//
-
-// Remove top margins from headings
-//
-// By default, `<h1>`-`<h6>` all receive top and bottom margins. We nuke the top
-// margin for easier control within type scales as it avoids margin collapsing.
-// stylelint-disable-next-line selector-list-comma-newline-after
-// h1, h2, h3, h4, h5, h6 {
-//   margin-top: 0;
-//   margin-bottom: $headings-margin-bottom;
-// }
-
-// Reset margins on paragraphs
-//
-// Similarly, the top margin on `<p>`s get reset. However, we also reset the
-// bottom margin to use `rem` units instead of `em`.
-// p {
-//   margin-top: 0;
-//   margin-bottom: $paragraph-margin-bottom;
-// }
-
-// Abbreviations
-//
-// 1. Duplicate behavior to the data-* attribute for our tooltip plugin
-// 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
-// 3. Add explicit cursor to indicate changed behavior.
-// 4. Remove the bottom border in Firefox 39-.
-// 5. Prevent the text-decoration to be skipped.
-
-// abbr[title],
-// abbr[data-original-title] { // 1
-//   text-decoration: underline; // 2
-//   text-decoration: underline dotted; // 2
-//   cursor: help; // 3
-//   border-bottom: 0; // 4
-//   text-decoration-skip-ink: none; // 5
-// }
-
-// address {
-//   margin-bottom: 1rem;
-//   font-style: normal;
-//   line-height: inherit;
-// }
-
-// ol,
-// ul,
-// dl {
-//   margin-top: 0;
-//   margin-bottom: 1rem;
-// }
-
-// ol ol,
-// ul ul,
-// ol ul,
-// ul ol {
-//   margin-bottom: 0;
-// }
-
-// dt {
-//   font-weight: $dt-font-weight;
-// }
-
-// dd {
-//   margin-bottom: .5rem;
-//   margin-left: 0; // Undo browser default
-// }
-
-// blockquote {
-//   margin: 0 0 1rem;
-// }
-
-// b,
-// strong {
-//   font-weight: $font-weight-bolder; // Add the correct font weight in Chrome, Edge, and Safari
-// }
-
-// small {
-//   @include font-size(80%); // Add the correct font size in all browsers
-// }
-
-//
-// Prevent `sub` and `sup` elements from affecting the line height in
-// all browsers.
-//
-
-// sub,
-// sup {
-//   position: relative;
-//   @include font-size(75%);
-//   line-height: 0;
-//   vertical-align: baseline;
-// }
-
-// sub { bottom: -.25em; }
-// sup { top: -.5em; }
-
 //
 // Links
 //
@@ -211,268 +54,3 @@ a {
 //     text-decoration: none;
 //   }
 // }
-
-//
-// Code
-//
-
-// pre,
-// code,
-// kbd,
-// samp {
-//   font-family: $font-family-monospace;
-//   @include font-size(1em); // Correct the odd `em` font sizing in all browsers.
-// }
-
-// pre {
-//   // Remove browser default top margin
-//   margin-top: 0;
-//   // Reset browser default of `1em` to use `rem`s
-//   margin-bottom: 1rem;
-//   // Don't allow content to break outside
-//   overflow: auto;
-// }
-
-//
-// Figures
-//
-
-// figure {
-//   // Apply a consistent margin strategy (matches our type styles).
-//   margin: 0 0 1rem;
-// }
-
-//
-// Images and content
-//
-
-// img {
-//   vertical-align: middle;
-//   border-style: none; // Remove the border on images inside links in IE 10-.
-// }
-
-// svg {
-//   // Workaround for the SVG overflow bug in IE10/11 is still required.
-//   // See https://github.com/twbs/bootstrap/issues/26878
-//   overflow: hidden;
-//   vertical-align: middle;
-// }
-
-//
-// Tables
-//
-
-// table {
-//   border-collapse: collapse; // Prevent double borders
-// }
-
-// caption {
-//   padding-top: $table-cell-padding;
-//   padding-bottom: $table-cell-padding;
-//   color: $table-caption-color;
-//   text-align: left;
-//   caption-side: bottom;
-// }
-
-// th {
-//   // Matches default `<td>` alignment by inheriting from the `<body>`, or the
-//   // closest parent with a set `text-align`.
-//   text-align: inherit;
-// }
-
-//
-// Forms
-//
-
-// label {
-//   // Allow labels to use `margin` for spacing.
-//   display: inline-block;
-//   margin-bottom: $label-margin-bottom;
-// }
-
-// Remove the default `border-radius` that macOS Chrome adds.
-//
-// Details at https://github.com/twbs/bootstrap/issues/24093
-// button {
-//   // stylelint-disable-next-line property-blacklist
-//   border-radius: 0;
-// }
-
-// Work around a Firefox/IE bug where the transparent `button` background
-// results in a loss of the default `button` focus styles.
-//
-// Credit: https://github.com/suitcss/base/
-// button:focus {
-//   outline: 1px dotted;
-//   outline: 5px auto -webkit-focus-ring-color;
-// }
-
-// input,
-// button,
-// select,
-// optgroup,
-// textarea {
-//   margin: 0; // Remove the margin in Firefox and Safari
-//   font-family: inherit;
-//   @include font-size(inherit);
-//   line-height: inherit;
-// }
-
-// button,
-// input {
-//   overflow: visible; // Show the overflow in Edge
-// }
-
-// button,
-// select {
-//   text-transform: none; // Remove the inheritance of text transform in Firefox
-// }
-
-// Remove the inheritance of word-wrap in Safari.
-//
-// Details at https://github.com/twbs/bootstrap/issues/24990
-// select {
-//   word-wrap: normal;
-// }
-
-// 1. Prevent a WebKit bug where (2) destroys native `audio` and `video`
-//    controls in Android 4.
-// 2. Correct the inability to style clickable types in iOS and Safari.
-// button,
-// [type="button"], // 1
-// [type="reset"],
-// [type="submit"] {
-//   -webkit-appearance: button; // 2
-// }
-
-// Opinionated: add "hand" cursor to non-disabled button elements.
-// @if $enable-pointer-cursor-for-buttons {
-//   button,
-//   [type="button"],
-//   [type="reset"],
-//   [type="submit"] {
-//     &:not(:disabled) {
-//       cursor: pointer;
-//     }
-//   }
-// }
-
-// Remove inner border and padding from Firefox, but don't restore the outline like Normalize.
-// button::-moz-focus-inner,
-// [type="button"]::-moz-focus-inner,
-// [type="reset"]::-moz-focus-inner,
-// [type="submit"]::-moz-focus-inner {
-//   padding: 0;
-//   border-style: none;
-// }
-
-// input[type="radio"],
-// input[type="checkbox"] {
-//   box-sizing: border-box; // 1. Add the correct box sizing in IE 10-
-//   padding: 0; // 2. Remove the padding in IE 10-
-// }
-
-// input[type="date"],
-// input[type="time"],
-// input[type="datetime-local"],
-// input[type="month"] {
-//   // Remove the default appearance of temporal inputs to avoid a Mobile Safari
-//   // bug where setting a custom line-height prevents text from being vertically
-//   // centered within the input.
-//   // See https://bugs.webkit.org/show_bug.cgi?id=139848
-//   // and https://github.com/twbs/bootstrap/issues/11266
-//   -webkit-appearance: listbox;
-// }
-
-// textarea {
-//   overflow: auto; // Remove the default vertical scrollbar in IE.
-//   // Textareas should really only resize vertically so they don't break their (horizontal) containers.
-//   resize: vertical;
-// }
-
-// fieldset {
-//   // Browsers set a default `min-width: min-content;` on fieldsets,
-//   // unlike e.g. `<div>`s, which have `min-width: 0;` by default.
-//   // So we reset that to ensure fieldsets behave more like a standard block element.
-//   // See https://github.com/twbs/bootstrap/issues/12359
-//   // and https://html.spec.whatwg.org/multipage/#the-fieldset-and-legend-elements
-//   min-width: 0;
-//   // Reset the default outline behavior of fieldsets so they don't affect page layout.
-//   padding: 0;
-//   margin: 0;
-//   border: 0;
-// }
-
-// 1. Correct the text wrapping in Edge and IE.
-// 2. Correct the color inheritance from `fieldset` elements in IE.
-// legend {
-//   display: block;
-//   width: 100%;
-//   max-width: 100%; // 1
-//   padding: 0;
-//   margin-bottom: .5rem;
-//   @include font-size(1.5rem);
-//   line-height: inherit;
-//   color: inherit; // 2
-//   white-space: normal; // 1
-// }
-
-// progress {
-//   vertical-align: baseline; // Add the correct vertical alignment in Chrome, Firefox, and Opera.
-// }
-
-// Correct the cursor style of increment and decrement buttons in Chrome.
-// [type="number"]::-webkit-inner-spin-button,
-// [type="number"]::-webkit-outer-spin-button {
-//   height: auto;
-// }
-
-// [type="search"] {
-//   // This overrides the extra rounded corners on search inputs in iOS so that our
-//   // `.form-control` class can properly style them. Note that this cannot simply
-//   // be added to `.form-control` as it's not specific enough. For details, see
-//   // https://github.com/twbs/bootstrap/issues/11586.
-//   outline-offset: -2px; // 2. Correct the outline style in Safari.
-//   -webkit-appearance: none;
-// }
-
-//
-// Remove the inner padding in Chrome and Safari on macOS.
-//
-
-// [type="search"]::-webkit-search-decoration {
-//   -webkit-appearance: none;
-// }
-
-//
-// 1. Correct the inability to style clickable types in iOS and Safari.
-// 2. Change font properties to `inherit` in Safari.
-//
-
-// ::-webkit-file-upload-button {
-//   font: inherit; // 2
-//   -webkit-appearance: button; // 1
-// }
-
-//
-// Correct element displays
-//
-
-// output {
-//   display: inline-block;
-// }
-
-// summary {
-//   display: list-item; // Add the correct display in all browsers
-//   cursor: pointer;
-// }
-
-// template {
-//   display: none; // Add the correct display in IE
-// }
-
-// Always hide an element with the `hidden` HTML attribute (from PureCSS).
-// Needed for proper display in IE 10-.
-// [hidden] {
-//   display: none !important;
-// }

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

@@ -1,3 +1,5 @@
+$theme-colors: map-merge($theme-colors, $colors);
+
 @each $color, $value in $theme-colors {
   @include bg-variant('.bg-#{$color}', $value);
 }

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

@@ -1,12 +1,12 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('App settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('App Settings')) }}{% endblock %}
 
 {% block head_warn_alert_siteurl_undefined %} {# remove including block for './widget/alert_siteurl_undefined.html' #}
 {% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('App settings') }}</h1>
+<h1 class="title">{{ t('App Settings') }}</h1>
 {% endblock %}
 
 

+ 2 - 2
src/server/views/admin/global-notification-detail.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Notification Settings')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('Notification settings') }}</h1>
+<h1 class="title">{{ t('Notification Settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

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

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

+ 2 - 2
src/server/views/admin/notification.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Notification settings')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Notification Settings')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('Notification settings') }}</h1>
+<h1 class="title">{{ t('Notification Settings') }}</h1>
 {% endblock %}
 
 {% block content_main %}

+ 2 - 2
src/server/views/admin/search.html

@@ -1,9 +1,9 @@
 {% extends '../layout/admin.html' %}
 
-{% block html_title %}{{ customizeService.generateCustomTitle(t('Full Text Search management')) }}{% endblock %}
+{% block html_title %}{{ customizeService.generateCustomTitle(t('Full Text Search Management')) }}{% endblock %}
 
 {% block content_header %}
-<h1 class="title">{{ t('Full Text Search management') }}</h1>
+<h1 class="title">{{ t('Full Text Search Management') }}</h1>
 {% endblock %}
 
 {% block content_main %}

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

@@ -80,14 +80,14 @@
     </ul>
 
     {# Brand Logo #}
-    <div class="navbar-brand mr-md-auto">
+    <div class="navbar-brand mr-0">
       <a class="grw-logo d-block" href="/">
         {% include '../widget/logo.html' %}
       </a>
     </div>
 
-    {# Navbar Center #}
-    <ul class="navbar-nav d-none d-md-block">
+    {# Search #}
+    <ul class="navbar-nav grw-navbar-search d-none d-md-block position-absolute">
       <li>
         {% if isSearchServiceConfigured() %}
         <div class="search-top" role="search" id="search-top"></div>

+ 1 - 50
src/server/views/modal/create_template.html

@@ -1,50 +1 @@
-{% set templateParentPath = parentPath(page.path | preventXss | escape) %}
-
-<div class="modal create-template" id="create-template">
-  <div class="modal-dialog">
-    <div class="modal-content">
-
-      <div class="modal-header bg-primary text-light">
-        <div class="modal-title">{{ t('template.modal_label.Create/Edit Template Page') }}</div>
-        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-      </div>
-      <div class="modal-body">
-        <div class="form-group">
-          <label class="mb-4">{{ t('template.modal_label.Create template under', templateParentPath ) }}</label>
-          <div class="row">
-            <div class="col-md-6">
-              <div class="card card-select-template">
-                <div class="card-header">{{ t('template.children.label') }}</div>
-                <div class="card-body">
-                  <p class="text-center"><code>_template</code></p>
-                  <p class="form-text text-muted text-center"><small>{{ t('template.children.desc') }}</small></p>
-                </div>
-                <div class="card-footer text-center">
-                  <a href="{{templateParentPath}}_template#edit"
-                      class="btn btn-sm btn-primary" id="template-button-children">
-                      {{ t("Edit") }}
-                  </a>
-                </div>
-              </div>
-            </div>
-            <div class="col-md-6">
-              <div class="card card-select-template">
-                <div class="card-header">{{ t('template.decendants.label') }}</div>
-                <div class="card-body">
-                  <p class="text-center"><code>__template</code></p>
-                  <p class="form-text text-muted text-center"><small>{{ t('template.decendants.desc') }}</small></p>
-                </div>
-                <div class="card-footer text-center">
-                  <a href="{{templateParentPath}}__template#edit"
-                      class="btn btn-sm btn-primary" id="template-button-decendants">
-                      {{ t("Edit") }}
-                  </a>
-                </div>
-              </div>
-            </div>
-          </div>
-        </div>
-      </div>
-    </div><!-- /.modal-content -->
-  </div><!-- /.modal-dialog -->
-</div><!-- /.modal -->
+<div id ="create-template-modal"></div>

+ 0 - 74
src/server/views/modal/delete.html

@@ -1,74 +0,0 @@
-  <div class="modal" id="deletePage">
-    <div class="modal-dialog">
-      <div class="modal-content">
-
-      <form role="form" id="delete-page-form" onsubmit="return false;">
-
-        <div class="modal-header text-light {% if page.isDeleted() %}bg-danger{% else %}bg-primary{% endif %}">
-          <div class="modal-title">
-            {% if page.isDeleted() %}
-            <i class="icon-fw icon-fire"></i> {{ t('modal_delete.delete_completely') }}
-            {% else %}
-            <i class="icon-fw icon-trash"></i> {{ t('modal_delete.delete_page') }}
-            {% endif %}
-          </div>
-          <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
-        </div>
-        <div class="modal-body">
-          <div class="form-group">
-            <label for="">{{ t('modal_delete.deleting_page') }}:</label><br>
-            <code>{{ page.path }}</code>
-          </div>
-
-          <hr>
-
-          {% if page.grant != 2 %}
-          <div class="custom-control custom-checkbox custom-checkbox-warning">
-            <input class="custom-control-input" name="recursively" id="cbDeleteRecursively" value="1" type="checkbox" checked>
-            <label class="custom-control-label" for="cbDeleteRecursively">
-              {{ t('modal_delete.delete_recursively') }}
-              <p class="form-text text-muted mt-0"> {{ t('modal_delete.recursively', page.path) }}</p>
-            </label>
-          </div>
-          {% endif %}
-          {% if not page.isDeleted() %}
-          <div class="custom-control custom-checkbox custom-checkbox-danger">
-            <input class="custom-control-input" name="completely" id="cbDeleteCompletely" {% if !user.canDeleteCompletely(page.creator._id) %} disabled="disabled" {% endif %} value="1"  type="checkbox">
-            <label class="custom-control-label" for="cbDeleteCompletely" class="text-danger">
-              {{ t('modal_delete.delete_completely') }}
-              <p class="form-text text-muted mt-0"> {{ t('modal_delete.completely') }}</p>
-            </label>
-            {% if !user.canDeleteCompletely(page.creator._id) %}
-              <p class="alert alert-warning p-2 my-0"><i class="icon-ban icon-fw" ></i>{{ t('modal_delete.delete_completely_restriction') }}</p>
-            {% endif %}
-          </div>
-          {% endif %}
-        </div>
-        <div class="modal-footer">
-          <div class="d-flex justify-content-between">
-            {% include '../widget/modal/page-api-error-messages.html' %}
-            <div>
-              <input type="hidden" name="_csrf" value="{{ csrf() }}">
-              <input type="hidden" name="path" value="{{ page.path }}">
-              <input type="hidden" name="page_id" value="{{ page._id.toString() }}">
-              <input type="hidden" name="revision_id" value="{{ page.revision._id.toString() }}">
-              {% if page.isDeleted() %}
-                <input type="hidden" name="completely" value="true">
-                <button type="submit" class="m-l-10 btn btn-danger delete-button">
-                  <i class="icon-fire" aria-hidden="true"></i>
-                  {{ t('modal_delete.delete_completely') }}
-                </button>
-              {% else %}
-                <button type="submit" class="m-l-10 btn btn-primary delete-button">
-                  <i class="icon-trash" aria-hidden="true"></i>
-                  {{ t('Delete') }}
-                </button>
-              {% endif %}
-            </div>
-          </div>
-        </div><!-- /.modal-footer -->
-
-      </form>
-      </div><!-- /.modal-content -->
-    </div><!-- /.modal-dialog -->
-  </div><!-- /.modal -->

+ 5 - 5
src/server/views/modal/empty_trash.html

@@ -14,15 +14,15 @@
         <ul>
           {% for data in pages %}
             {% if pagePropertyName %}
-              {% set page = data[pagePropertyName] %}
+              {% set deletePage = data[pagePropertyName] %}
             {% else %}
-              {% set page = data %}
+              {% set deletePage = data %}
             {% endif %}
             <li>
-              <img src="{{ page.lastUpdateUser|picture }}" class="picture img-circle">
-              <a href="{{ page.path }}"
+              <img src="{{ deletePage.lastUpdateUser|picture }}" class="picture img-circle">
+              <a href="{{ deletePage.path }}"
                 class="page-list-link"
-                data-path="{{ page.path }}">{{ decodeURIComponent(page.path) }}
+                data-path="{{ deletePage.path }}">{{ decodeURIComponent(deletePage.path) }}
               </a>
             </li>
           {% endfor %}

+ 1 - 6
src/server/views/widget/page_alerts.html

@@ -40,15 +40,10 @@
         {% endif %}
       </span>
       {% if user and not page.isDeleted() %}
-      <form role="form" id="unlink-page-form" onsubmit="return false;">
-        <input type="hidden" name="_csrf" value="{{ csrf() }}">
-        {# TODO: should be removed by GW-2283 #}
-        <input type="hidden" name="path" value="{{ page.path }}">
-        <button type="submit" class="btn btn-outline-secondary btn-sm float-right">
+        <button type="button" id="unlink-page-button" class="btn btn-secondary btn-sm float-right">
           <i class="ti-unlink" aria-hidden="true"></i>
           Unlink
         </button>
-      </form>
       {% endif %}
     </div>
     {% endif %}

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

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

+ 16 - 16
src/server/views/widget/page_list.html

@@ -2,44 +2,44 @@
 {% for data in pages %}
 
 {% if pagePropertyName %}
-  {% set page = data[pagePropertyName] %}
+  {% set listPage = data[pagePropertyName] %}
 {% else %}
-  {% set page = data %}
+  {% set listPage = data %}
 {% endif %}
 
 <li>
-  <img src="{{ page.lastUpdateUser|picture }}" class="picture rounded-circle">
-  <a href="{{ encodeURI(page.path) }}" class="text-break ml-1">
-    {{ page.path | preventXss }}
+  <img src="{{ listPage.lastUpdateUser|picture }}" class="picture rounded-circle">
+  <a href="{{ encodeURI(listPage.path) }}" class="text-break ml-1">
+    {{ listPage.path | preventXss }}
   </a>
   <span class="page-list-meta">
-    {% if page.isTopPage() %}
+    {% if listPage.isTopPage() %}
       <span class="badge badge-info">TOP</span>
     {% endif  %}
 
-    {% if page.isTemplate() %}
+    {% if listPage.isTemplate() %}
       <span class="badge badge-info">TMPL</span>
     {% endif  %}
 
-    {% if page.commentCount > 0 %}
+    {% if listPage.commentCount > 0 %}
     <span>
-      <i class="icon-bubble"></i>{{ page.commentCount }}
+      <i class="icon-bubble"></i>{{ listPage.commentCount }}
     </span>
     {% endif  %}
 
-    {% if page.liker.length > 0 %}
-    <span class="page-list-liker" data-count="{{ page.liker.length }}">
-      <i class="icon-like"></i>{{ page.liker.length }}
+    {% if listPage.liker.length > 0 %}
+    <span class="page-list-liker" data-count="{{ listPage.liker.length }}">
+      <i class="icon-like"></i>{{ listPage.liker.length }}
     </span>
     {% endif  %}
 
-    {% if viewConfig.seener_threshold and page.seenUsers.length >= viewConfig.seener_threshold %}
-    <span class="page-list-seer" data-count="{{ page.seenUsers.length }}">
-      <i class="fa fa-paw"></i>{{ page.seenUsers.length }}
+    {% if viewConfig.seener_threshold and listPage.seenUsers.length >= viewConfig.seener_threshold %}
+    <span class="page-list-seer" data-count="{{ listPage.seenUsers.length }}">
+      <i class="fa fa-paw"></i>{{ listPage.seenUsers.length }}
     </span>
     {% endif  %}
 
-    {% if !page.isPublic() %}
+    {% if !listPage.isPublic() %}
     <span>
       <i class="icon icon-lock"></i>
     </span>

+ 0 - 1
src/server/views/widget/page_modals.html

@@ -1,6 +1,5 @@
 {% include '../modal/rename.html' %}
 {% include '../modal/empty_trash.html' %}
-{% include '../modal/delete.html' %}
 {% include '../modal/create_template.html' %}
 {% include '../modal/duplicate.html' %}
 {% include '../modal/put_back.html' %}

+ 4 - 4
yarn.lock

@@ -3040,10 +3040,10 @@ boom@5.x.x:
   dependencies:
     hoek "4.x.x"
 
-bootstrap@^4.4.1:
-  version "4.4.1"
-  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.4.1.tgz#8582960eea0c5cd2bede84d8b0baf3789c3e8b01"
-  integrity sha512-tbx5cHubwE6e2ZG7nqM3g/FZ5PQEDMWmMGNrCUBVRPHXTJaH7CBDdsLeu3eCh3B1tzAxTnAbtmrzvWEvT2NNEA==
+bootstrap@^4.5.0:
+  version "4.5.0"
+  resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.5.0.tgz#97d9dbcb5a8972f8722c9962483543b907d9b9ec"
+  integrity sha512-Z93QoXvodoVslA+PWNdk23Hze4RBYIkpb5h8I2HY2Tu2h7A0LpAgLcyrhrSUyo2/Oxm2l1fRZPs1e5hnxnliXA==
 
 brace-expansion@^1.1.7:
   version "1.1.8"