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

Merge branch 'support/apply-bootstrap4' into support/reactify-login-page-stock

yusuketk 5 лет назад
Родитель
Сommit
9b89772b32
76 измененных файлов с 1125 добавлено и 344 удалено
  1. 7 0
      .github/workflows/build.yml
  2. 14 1
      CHANGES.md
  3. 1 1
      bin/download-cdn-resources.js
  4. 1 1
      config/webpack.common.js
  5. 66 64
      resource/locales/en-US/admin/admin.json
  6. 83 74
      resource/locales/en-US/translation.json
  7. 2 2
      resource/locales/en-US/welcome.md
  8. 4 2
      resource/locales/ja/admin/admin.json
  9. 15 6
      resource/locales/ja/translation.json
  10. 14 8
      src/client/js/admin.jsx
  11. 9 4
      src/client/js/app.jsx
  12. 6 0
      src/client/js/bootstrap.jsx
  13. 2 2
      src/client/js/components/Admin/App/AwsSetting.jsx
  14. 2 2
      src/client/js/components/Admin/Customize/CustomizeLayoutOptions.jsx
  15. 1 1
      src/client/js/components/Admin/Customize/CustomizeScriptSetting.jsx
  16. 1 1
      src/client/js/components/Admin/Customize/CustomizeTitle.jsx
  17. 2 2
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx
  18. 1 1
      src/client/js/components/Admin/ImportData/GrowiArchive/ImportForm.jsx
  19. 2 2
      src/client/js/components/Admin/MarkdownSetting/XssForm.jsx
  20. 3 3
      src/client/js/components/Admin/Notification/NotificationSetting.jsx
  21. 1 1
      src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx
  22. 2 2
      src/client/js/components/Admin/Security/LocalSecuritySetting.jsx
  23. 2 2
      src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx
  24. 1 1
      src/client/js/components/Admin/Users/UserTable.jsx
  25. 56 0
      src/client/js/components/ErrorBoudary.jsx
  26. 31 0
      src/client/js/components/FormattedDistanceDate.jsx
  27. 1 1
      src/client/js/components/Me/AssociateModal.jsx
  28. 2 2
      src/client/js/components/Me/ExternalAccountLinkedMe.jsx
  29. 4 1
      src/client/js/components/Me/PersonalSettings.jsx
  30. 1 1
      src/client/js/components/MyDraftList/MyDraftList.jsx
  31. 43 0
      src/client/js/components/Navbar/PageCreateButton.jsx
  32. 6 6
      src/client/js/components/PageComment/Comment.jsx
  33. 229 0
      src/client/js/components/PageCreateModal.jsx
  34. 1 1
      src/client/js/components/PageEditor/Editor.jsx
  35. 1 1
      src/client/js/components/SavePageControls/GrantSelector.jsx
  36. 12 4
      src/client/js/components/SearchPage/DeletePageListModal.jsx
  37. 10 5
      src/client/js/components/SearchPage/SearchResult.jsx
  38. 76 11
      src/client/js/components/Sidebar/History.jsx
  39. 1 1
      src/client/js/components/StaffCredit/Contributor.js
  40. 6 6
      src/client/js/components/StaffCredit/StaffCredit.jsx
  41. 1 1
      src/client/js/components/User/UserPicture.jsx
  42. 1 0
      src/client/js/legacy/crowi.js
  43. 20 0
      src/client/js/services/AppContainer.js
  44. 1 1
      src/client/styles/scss/_admin.scss
  45. 7 0
      src/client/styles/scss/_layout_kibela.scss
  46. 4 0
      src/client/styles/scss/_login.scss
  47. 14 0
      src/client/styles/scss/_page-path.scss
  48. 0 2
      src/client/styles/scss/_sidebar.scss
  49. 10 11
      src/client/styles/scss/_staff_credit.scss
  50. 1 0
      src/client/styles/scss/style-app.scss
  51. 0 12
      src/client/styles/scss/theme/_apply-colors-dark.scss
  52. 5 0
      src/client/styles/scss/theme/_apply-colors-kibela.scss
  53. 0 12
      src/client/styles/scss/theme/_apply-colors-light.scss
  54. 15 8
      src/client/styles/scss/theme/_apply-colors.scss
  55. 3 0
      src/client/styles/scss/theme/antarctic.scss
  56. 5 2
      src/client/styles/scss/theme/christmas.scss
  57. 6 0
      src/client/styles/scss/theme/default.scss
  58. 3 0
      src/client/styles/scss/theme/future.scss
  59. 3 0
      src/client/styles/scss/theme/halloween.scss
  60. 181 6
      src/client/styles/scss/theme/island.scss
  61. 6 0
      src/client/styles/scss/theme/kibela.scss
  62. 7 7
      src/client/styles/scss/theme/mono-blue.scss
  63. 3 0
      src/client/styles/scss/theme/nature.scss
  64. 26 9
      src/client/styles/scss/theme/spring.scss
  65. 3 2
      src/client/styles/scss/theme/wood.scss
  66. 15 9
      src/lib/components/PagePathHierarchicalLink.jsx
  67. 10 19
      src/server/models/page.js
  68. 40 0
      src/server/routes/apiv3/pages.js
  69. 1 1
      src/server/service/import.js
  70. 1 1
      src/server/service/search-delegator/elasticsearch.js
  71. 2 2
      src/server/views/admin/index.html
  72. 3 0
      src/server/views/layout/layout.html
  73. 2 1
      src/server/views/modal/create_page.html
  74. 1 1
      src/server/views/modal/delete.html
  75. 1 1
      src/server/views/widget/page_content.html
  76. 2 13
      src/test/models/page.test.js

+ 7 - 0
.github/workflows/build.yml

@@ -66,6 +66,13 @@ jobs:
         additional-tags: 'latest'
         additional-tags: 'latest'
         publish: true
         publish: true
 
 
+    - name: Slack Notification
+      uses: weseek/ghaction-release-slack-notification@master
+      with:
+        channel: '#general'
+        url: ${{ secrets.SLACK_WEBHOOK_URL }}
+        created_tag: v${{ env.SEMVER }}
+
     - name: Check whether workspace is clean
     - name: Check whether workspace is clean
       run: |
       run: |
         STATUS=`git status --porcelain`
         STATUS=`git status --porcelain`

+ 14 - 1
CHANGES.md

@@ -2,12 +2,25 @@
 
 
 ## v4.0.0-RC
 ## v4.0.0-RC
 
 
+### BREAKING CHANGES
+
+* Crowi Classic Behavior is removed
+* Crowi Classic Layout is removed
+* 'default-dark' theme is now merged as a dark mode variant of 'default' theme
+* 'blue-night' theme is now merged as a dark mode variant of 'mono-blue' theme
+
+### Updates
+
+* Feature: Switch Light/Dark Mode
+* Improvement: Migrate to Bootstrap 4
+* Improvement: Copy Page URL menu item to copy path dropdown
+* Improvement: Show contributors by Bootstrap Modal
 * Support: Upgrade libs
 * Support: Upgrade libs
     * bootstrap
     * bootstrap
 
 
 ## v3.8.1-RC
 ## v3.8.1-RC
 
 
-*
+* Fix: Unset overflow-y style for Edit Tags Modal
 
 
 ## v3.8.0
 ## v3.8.0
 
 

+ 1 - 1
bin/download-cdn-resources.js

@@ -27,7 +27,7 @@ const service = new CdnResourcesService();
 
 
 service.downloadAndWriteAll(downloader)
 service.downloadAndWriteAll(downloader)
   .then(() => {
   .then(() => {
-    logger.info('Download is terminated successfully');
+    logger.info('Download is completed successfully');
   })
   })
   .catch((err) => {
   .catch((err) => {
     logger.error(err);
     logger.error(err);

+ 1 - 1
config/webpack.common.js

@@ -41,7 +41,7 @@ module.exports = (options) => {
       'styles/theme-halloween':       './src/client/styles/scss/theme/halloween.scss',
       'styles/theme-halloween':       './src/client/styles/scss/theme/halloween.scss',
       'styles/theme-christmas':          './src/client/styles/scss/theme/christmas.scss',
       'styles/theme-christmas':          './src/client/styles/scss/theme/christmas.scss',
       'styles/theme-wood':          './src/client/styles/scss/theme/wood.scss',
       'styles/theme-wood':          './src/client/styles/scss/theme/wood.scss',
-      // 'styles/theme-island':      './src/client/styles/scss/theme/island.scss',
+      'styles/theme-island':      './src/client/styles/scss/theme/island.scss',
       'styles/theme-antarctic':      './src/client/styles/scss/theme/antarctic.scss',
       'styles/theme-antarctic':      './src/client/styles/scss/theme/antarctic.scss',
       'styles/theme-spring':         './src/client/styles/scss/theme/spring.scss',
       'styles/theme-spring':         './src/client/styles/scss/theme/spring.scss',
       // styles for external services
       // styles for external services

+ 66 - 64
resource/locales/en-US/admin/admin.json

@@ -1,35 +1,35 @@
 {
 {
   "admin_top": {
   "admin_top": {
     "management_wiki": "Management Wiki",
     "management_wiki": "Management Wiki",
-    "system_information": "System Information",
-    "wiki_administrator": "Only Wiki administrator can access this page",
-    "assign_administrator": "You can assign administrator from Assign administrator button in the User management page",
+    "system_information": "System information",
+    "wiki_administrator": "Only wiki administrator can access this page",
+    "assign_administrator": "You can assign the selected user to be a wiki administrator on the User Management page using the 'Give admin access' button",
     "list_of_installed_plugins": "List of installed plugins",
     "list_of_installed_plugins": "List of installed plugins",
     "package_name": "Package name",
     "package_name": "Package name",
     "specified_version": "Specified version",
     "specified_version": "Specified version",
     "installed_version": "Installed version",
     "installed_version": "Installed version",
     "list_of_env_vars":"List of environment variables",
     "list_of_env_vars":"List of environment variables",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
     "env_var_priority": "For environment variables other than security, the value of the database is obtained preferentially.",
-    "about_security": "Check <a href='/admin/security'>Securtiy Management</a> for security environment variables."
+    "about_security": "Check <a href='/admin/security'>Securtiy Settings</a> for security environment variables."
   },
   },
   "app_setting": {
   "app_setting": {
     "site_name": "Site name",
     "site_name": "Site name",
-    "sitename_change": "You can change Site Name which is used for header and HTML title.",
+    "sitename_change": "You can change site name which is used for header and HTML title.",
     "header_content": "The contents entered here will be shown in the header etc.",
     "header_content": "The contents entered here will be shown in the header etc.",
     "site_url_desc": "This is for the site URL setting.",
     "site_url_desc": "This is for the site URL setting.",
     "site_url_warn": "Some features don't work because the site URL is not set.",
     "site_url_warn": "Some features don't work because the site URL is not set.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "confidential_name": "Confidential name",
     "confidential_name": "Confidential name",
     "confidential_example": "ex): internal use only",
     "confidential_example": "ex): internal use only",
-    "default_language": "Default Language for new users",
-    "file_uploading": "File Uploading",
-    "enable_files_except_image": "Enable file upload other than image files.",
+    "default_language": "Default language for new users",
+    "file_uploading": "File uploading",
+    "enable_files_except_image": "Enabling this option will allow upload of any file type. Without this option, only image file upload is supported.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
     "update": "Update",
     "update": "Update",
     "mail_settings": "Mail settings",
     "mail_settings": "Mail settings",
     "smtp_used": "If you have SMTP settings, it will be used.",
     "smtp_used": "If you have SMTP settings, it will be used.",
     "smtp_but_aws": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
     "smtp_but_aws": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
-    "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
+    "neihter_of": "If neither is selected, then no email will be sent.",
     "from_e-mail_address": "From e-mail address",
     "from_e-mail_address": "From e-mail address",
     "smtp_settings": "SMTP settings",
     "smtp_settings": "SMTP settings",
     "host": "Host",
     "host": "Host",
@@ -51,18 +51,18 @@
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used."
     "use_env_var_if_empty": "If the value in the database is empty, the value of the environment variable <cod>{{variable}}</code> is used."
   },
   },
   "markdown_setting": {
   "markdown_setting": {
-    "lineBreak_header": "Line Break Setting",
+    "lineBreak_header": "Line break setting",
     "lineBreak_desc": "You can change line break settings.",
     "lineBreak_desc": "You can change line break settings.",
     "lineBreak_options": {
     "lineBreak_options": {
-      "enable_lineBreak": "Enable Line Break",
-      "enable_lineBreak_desc": "Treat line break in the text page as<code>&lt;br&gt;</code>in HTML",
-      "enable_lineBreak_for_comment": "Enable Line Break in comment",
-      "enable_lineBreak_for_comment_desc": "Treat line break in comment as<code>&lt;br&gt;</code>in HTML"
+      "enable_lineBreak": "Enable line break",
+      "enable_lineBreak_desc": "Convert line break in the text page to<code>&lt;br&gt;</code>in HTML",
+      "enable_lineBreak_for_comment": "Enable line break in comment",
+      "enable_lineBreak_for_comment_desc": "Convert line break in comment to<code>&lt;br&gt;</code>in HTML"
     },
     },
-    "presentation_header": "Presentation Setting",
+    "presentation_header": "Presentation setting",
     "presentation_desc": "You can change presentation settings.",
     "presentation_desc": "You can change presentation settings.",
     "presentation_options": {
     "presentation_options": {
-      "page_break_setting": "Page break Setting",
+      "page_break_setting": "Page break setting",
       "preset_one_separator": "Preset 1",
       "preset_one_separator": "Preset 1",
       "preset_one_separator_desc": "3 Blank lines",
       "preset_one_separator_desc": "3 Blank lines",
       "preset_one_separator_value": "\\n\\n\\n",
       "preset_one_separator_value": "\\n\\n\\n",
@@ -70,16 +70,16 @@
       "preset_two_separator_desc": "5 Hyphens",
       "preset_two_separator_desc": "5 Hyphens",
       "preset_two_separator_value": "-----",
       "preset_two_separator_value": "-----",
       "custom_separator": "Custom",
       "custom_separator": "Custom",
-      "custom_separator_desc": "Regular Expression"
+      "custom_separator_desc": "Regular expression"
     },
     },
-    "xss_header": "Prevent XSS(Cross Site Scripting) Setting",
+    "xss_header": "Prevent XSS(cross site scripting) setting",
     "xss_desc": "You can change the handling of HTML tags in markdown text.",
     "xss_desc": "You can change the handling of HTML tags in markdown text.",
     "xss_options": {
     "xss_options": {
-      "enable_xss_prevention": "Enable XSS Prevention",
-      "ignore_all_tags": "Ignore All Tags",
-      "ignore_all_tags_desc": "Stripe all HTML tags and attributes",
-      "recommended_setting": "Recommended Setting",
-      "custom_whitelist": "Custom Whitelist",
+      "enable_xss_prevention": "Enable XSS prevention",
+      "remove_all_tags": "Remove all tags",
+      "remove_all_tags_desc": "Stripe all HTML tags and attributes",
+      "recommended_setting": "Recommended setting",
+      "custom_whitelist": "Custom whitelist",
       "tag_names": "Tag names",
       "tag_names": "Tag names",
       "tag_attributes": "Tag attributes",
       "tag_attributes": "Tag attributes",
       "import_recommended": "Import recommended {{target}}"
       "import_recommended": "Import recommended {{target}}"
@@ -90,11 +90,11 @@
     "layout": "Layout",
     "layout": "Layout",
     "theme": "Theme",
     "theme": "Theme",
     "layout_desc": {
     "layout_desc": {
-      "growi_title": "Simple and Clear",
+      "growi_title": "Simple and clear",
       "growi_text1": "Full screen layout and thin margins/paddings",
       "growi_text1": "Full screen layout and thin margins/paddings",
       "growi_text2": "Show and post comments at the bottom of the page",
       "growi_text2": "Show and post comments at the bottom of the page",
-      "growi_text3": "Affix Table-of-contents",
-      "kibela_title": "Easy Viewing Structure",
+      "growi_text3": "Affix table-of-contents",
+      "kibela_title": "Easy viewing structure",
       "kibela_text1": "Center aligned contents",
       "kibela_text1": "Center aligned contents",
       "kibela_text2": "Show and post comments at the bottom of the page",
       "kibela_text2": "Show and post comments at the bottom of the page",
       "kibela_text3": "Affix Table-of-contents"
       "kibela_text3": "Affix Table-of-contents"
@@ -107,30 +107,30 @@
     "function_desc": "You can choose Valid/Invalid of the function",
     "function_desc": "You can choose Valid/Invalid of the function",
     "function_options": {
     "function_options": {
       "timeline": "Timeline function",
       "timeline": "Timeline function",
-      "timeline_desc1": "You can show the timeline of the subpages.",
-      "timeline_desc2": "If there are many subpages, performance decreases while page loading.",
+      "timeline_desc1": "You can show the timeline of the child pages.",
+      "timeline_desc2": "If there are many child pages, performance decreases while page loading.",
       "timeline_desc3": "You can speed up list page display by invalidating.",
       "timeline_desc3": "You can speed up list page display by invalidating.",
       "tab_switch": "Save tab-switching in the browser",
       "tab_switch": "Save tab-switching in the browser",
       "tab_switch_desc1": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
       "tab_switch_desc1": "Save edit tab and history tab switching in the browser and make it object for forward/back command of the browser.",
       "tab_switch_desc2": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
       "tab_switch_desc2": "By invalidating, you can make page transition as the only object for forward/back command of the browser.",
       "attach_title_header": "Add h1 section when create new page automatically",
       "attach_title_header": "Add h1 section when create new page automatically",
       "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
       "attach_title_header_desc": "Add page path to the first line as h1 section when create new page",
-      "recent_created__n_draft_num_desc": "Number of Recently Created Pages & Drafts Displayed",
+      "recent_created__n_draft_num_desc": "Number of recently created pages & drafts displayed",
       "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page",
       "recently_created_n_draft_num_desc": "Number of recently created pages and drafts displayed on user page",
-      "stale_notification": "Display Notification on Stale Pages",
+      "stale_notification": "Display notification on stale pages",
       "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
       "stale_notification_desc": "Displays the notification to pages more than 1 year since the last update.",
       "show_all_reply_comments": "Show all reply comments",
       "show_all_reply_comments": "Show all reply comments",
       "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted."
       "show_all_reply_comments_desc": "When the setting value is off, comments other than the latest two are omitted."
     },
     },
-    "code_highlight": "Code Highlight",
+    "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.",
     "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": "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.<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_header": "Custom HTML Header",
+    "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_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",
     "custom_css": "Custom CSS",
     "write_css": "You can write CSS that is applied to whole system.",
     "write_css": "You can write CSS that is applied to whole system.",
-    "ctrl_space": "Ctrl+Space to Autocomplete",
+    "ctrl_space": "Ctrl+Space to autocomplete",
     "custom_script": "Custom script",
     "custom_script": "Custom script",
     "write_java": "You can write Javascript that is applied to whole system.",
     "write_java": "You can write Javascript that is applied to whole system.",
     "reflect_change": "You need to reload the page to reflect the change."
     "reflect_change": "You need to reload the page to reflect the change."
@@ -138,7 +138,7 @@
   "importer_management": {
   "importer_management": {
     "beta_warning": "This function is Beta.",
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
     "import_from": "Import from {{from}}",
-    "import_growi_archive": "Import GROWI Archive",
+    "import_growi_archive": "Import GROWI archive",
     "growi_settings": {
     "growi_settings": {
       "description_of_import_mode": {
       "description_of_import_mode": {
         "about": "When you import data with the same name as an existing one, choose from the following three modes below.",
         "about": "When you import data with the same name as an existing one, choose from the following three modes below.",
@@ -151,7 +151,7 @@
       "extracted_file": "Extracted File",
       "extracted_file": "Extracted File",
       "collection": "Collection",
       "collection": "Collection",
       "upload": "Upload",
       "upload": "Upload",
-      "discard": "Discard Uploaded Data",
+      "discard": "Discard uploaded data",
       "errors": {
       "errors": {
         "at_least_one": "Select one or more collections.",
         "at_least_one": "Select one or more collections.",
         "page_and_revision": "'Pages' and 'Revisions' must be imported both.",
         "page_and_revision": "'Pages' and 'Revisions' must be imported both.",
@@ -196,7 +196,7 @@
     },
     },
     "import": "Import",
     "import": "Import",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
-    "Directory_hierarchy_tag": "Directory Hierarchy Tag"
+    "Directory_hierarchy_tag": "Directory hierarchy tag"
   },
   },
   "export_management": {
   "export_management": {
     "exporting_collection_list": "Exporting Collection List",
     "exporting_collection_list": "Exporting Collection List",
@@ -217,11 +217,11 @@
     "delete": "Delete"
     "delete": "Delete"
   },
   },
   "user_management": {
   "user_management": {
-    "invite_users": "Invite New Users",
+    "invite_users": "Invite new users",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "click_twice_same_checkbox": "You should check at least one checkbox.",
     "invite_modal": {
     "invite_modal": {
       "emails": "Emails",
       "emails": "Emails",
-      "invite_thru_email": "Send Invitation Email",
+      "invite_thru_email": "Send invitation email",
       "valid_email": "Valid email address is required",
       "valid_email": "Valid email address is required",
       "temporary_password": "The created user has a temporary password",
       "temporary_password": "The created user has a temporary password",
       "send_new_password": "Please send the new password to the user.",
       "send_new_password": "Please send the new password to the user.",
@@ -230,15 +230,15 @@
     },
     },
     "user_table": {
     "user_table": {
       "administrator": "Administrator",
       "administrator": "Administrator",
-      "edit_menu": "Edit Menu",
-      "reset_password": "Reset Password",
+      "edit_menu": "Edit menu",
+      "reset_password": "Reset password",
       "administrator_menu": "Administrator Menu",
       "administrator_menu": "Administrator Menu",
       "accept": "Accept",
       "accept": "Accept",
-      "deactivate_account": "Deactivate Account",
+      "deactivate_account": "Deactivate account",
       "your_own": "You cannot deactivate your own account",
       "your_own": "You cannot deactivate your own account",
-      "remove_admin_access": "Remove Admin Access",
+      "remove_admin_access": "Remove admin access",
       "cannot_remove": "You cannot remove yourself from administrator",
       "cannot_remove": "You cannot remove yourself from administrator",
-      "give_admin_access": "Give Admin Access"
+      "give_admin_access": "Give admin access"
     },
     },
     "reset_password": "Reset Password",
     "reset_password": "Reset Password",
     "reset_password_modal": {
     "reset_password_modal": {
@@ -249,13 +249,15 @@
       "new_password": "New Password"
       "new_password": "New Password"
     },
     },
     "external_account": "External Account Management",
     "external_account": "External Account Management",
+    "external_accounts":"External accounts",
+    "create_external_account":"Create external account",
     "external_account_list": "External Account List",
     "external_account_list": "External Account List",
     "invite": "Invite",
     "invite": "Invite",
     "invited": "User was invited",
     "invited": "User was invited",
     "back_to_user_management": "Back to User Management",
     "back_to_user_management": "Back to User Management",
-    "authentication_provider": "Authentication Provider",
+    "authentication_provider": "Authentication provider",
     "manage": "Manage",
     "manage": "Manage",
-    "password_setting": "Password Setting",
+    "password_setting": "Password setting",
     "password_setting_help": "Is password set?",
     "password_setting_help": "Is password set?",
     "set": "Yes",
     "set": "Yes",
     "unset": "No",
     "unset": "No",
@@ -264,34 +266,34 @@
     "current_users": "Current users:"
     "current_users": "Current users:"
   },
   },
   "user_group_management": {
   "user_group_management": {
-    "create_group": "Create New Group",
-    "deny_create_group": "You can't create a new group with the current settings",
-    "group_name": "Group Name",
+    "create_group": "Create new group",
+    "deny_create_group": "You can't create a new group with the current settings.",
+    "group_name": "Group name",
     "group_example": "e.g. : Group1",
     "group_example": "e.g. : Group1",
     "add_modal": {
     "add_modal": {
-      "add_user": "Add a User to the Created Group",
-      "search_option": "Search Option",
+      "add_user": "Add a user to the created group",
+      "search_option": "Search option",
       "enable_option": "Enable {{option}}",
       "enable_option": "Enable {{option}}",
-      "forward_match": "forword match",
-      "partial_match": "partial match",
-      "backward_match": "backward match"
+      "forward_match": "Forword match",
+      "partial_match": "Partial match",
+      "backward_match": "Backward match"
     },
     },
-    "group_list": "Group List",
-    "back_to_list": "Go Back to Group List",
-    "basic_info": "Basic Info",
-    "user_list": "User List",
+    "group_list": "Group list",
+    "back_to_list": "Go back to group list",
+    "basic_info": "Basic info",
+    "user_list": "User list",
     "created_group": "Group was created",
     "created_group": "Group was created",
-    "is_loading_data": "fetch data...",
-    "no_pages": "There are no pages the group has view permission",
+    "is_loading_data": "Fetch data...",
+    "no_pages": "There are no pages the group has view permission.",
     "remove_from_group": "Remove this user",
     "remove_from_group": "Remove this user",
     "delete_modal": {
     "delete_modal": {
-      "header": "Delete Group",
-      "desc": "Once deleted, the deleted group and its private pages cannot be retrieved",
+      "header": "Delete group",
+      "desc": "Once deleted, the deleted group and its private pages cannot be retrieved.",
       "dropdown_desc": "Choose an action for private pages",
       "dropdown_desc": "Choose an action for private pages",
       "select_group": "Select a group",
       "select_group": "Select a group",
       "no_groups": "No groups to select",
       "no_groups": "No groups to select",
-      "publish_pages": "Publish All",
-      "delete_pages": "Delete All",
+      "publish_pages": "Publish all",
+      "delete_pages": "Delete all",
       "transfer_pages": "Transfer to another group"
       "transfer_pages": "Transfer to another group"
     }
     }
   }
   }

+ 83 - 74
resource/locales/en-US/translation.json

@@ -2,7 +2,7 @@
   "Help": "Help",
   "Help": "Help",
   "Edit": "Edit",
   "Edit": "Edit",
   "Delete": "Delete",
   "Delete": "Delete",
-  "Delete All": "Delete All",
+  "delete_all": "Delete all",
   "Duplicate": "Duplicate",
   "Duplicate": "Duplicate",
   "Copy": "Copy",
   "Copy": "Copy",
   "Click to copy": "Click to copy",
   "Click to copy": "Click to copy",
@@ -25,7 +25,7 @@
   "Undo": "Undo",
   "Undo": "Undo",
   "Article": "Article",
   "Article": "Article",
   "Page": "Page",
   "Page": "Page",
-  "Page Path": "Page Path",
+  "Page Path": "Page path",
   "Category": "Category",
   "Category": "Category",
   "User": "User",
   "User": "User",
   "status": "Status",
   "status": "Status",
@@ -50,11 +50,11 @@
   "username": "Username",
   "username": "Username",
   "Created": "Created",
   "Created": "Created",
   "Last updated": "Updated",
   "Last updated": "Updated",
-  "Last_Login": "Last Login",
+  "Last_Login": "Last login",
   "Share": "Share",
   "Share": "Share",
   "Share Link": "Share Link",
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
   "Markdown Link": "Markdown Link",
-  "Create/Edit Template": "Create/Edit Template Page",
+  "Create/Edit Template": "Create/Edit template page",
   "Go to this version": "View this version",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "View diff": "View diff",
   "No diff": "No diff",
   "No diff": "No diff",
@@ -62,16 +62,16 @@
   "User ID": "User ID",
   "User ID": "User ID",
   "User's Home": "User's Home",
   "User's Home": "User's Home",
   "User Settings": "User Settings",
   "User Settings": "User Settings",
-  "User Information": "User Information",
-  "Basic Info": "Basic Info",
+  "User Information": "User information",
+  "Basic Info": "Basic info",
   "Name": "Name",
   "Name": "Name",
   "Email": "Email",
   "Email": "Email",
   "Language": "Language",
   "Language": "Language",
   "English": "English",
   "English": "English",
   "Japanese": "Japanese",
   "Japanese": "Japanese",
-  "Set Profile Image": "Set Profile Image",
-  "Upload Image": "Upload Image",
-  "Current Image": "Current Image",
+  "Set Profile Image": "Set profile image",
+  "Upload Image": "Upload image",
+  "Current Image": "Current image",
   "Delete Image": "Delete Image",
   "Delete Image": "Delete Image",
   "Delete this image?": "Delete this image?",
   "Delete this image?": "Delete this image?",
   "Updated": "Updated",
   "Updated": "Updated",
@@ -86,10 +86,10 @@
   "Memo": "memo",
   "Memo": "memo",
   "Input page name": "Input page name",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
   "Input page name (optional)": "Input page name (optional)",
-  "New Page": "New Page",
+  "New Page": "New page",
   "Create under": "Create page under below:",
   "Create under": "Create page under below:",
   "Table of Contents": "Table of Contents",
   "Table of Contents": "Table of Contents",
-  "Management Wiki Home": "Management Wiki Home",
+  "Wiki Management Home Page": "Wiki Management Home Page",
   "App Settings": "App Settings",
   "App Settings": "App Settings",
   "Site URL settings": "Site URL settings",
   "Site URL settings": "Site URL settings",
   "Markdown Settings": "Markdown Settings",
   "Markdown Settings": "Markdown Settings",
@@ -108,7 +108,7 @@
   "Public": "Public",
   "Public": "Public",
   "Anyone with the link": "Anyone with the link",
   "Anyone with the link": "Anyone with the link",
   "Specified users only": "Specified users only",
   "Specified users only": "Specified users only",
-  "Just me": "Just me",
+  "Only me": "Only me",
   "Only inside the group": "Only inside the group",
   "Only inside the group": "Only inside the group",
   "page_list_and_search_results": "Page list / Search results",
   "page_list_and_search_results": "Page list / Search results",
   "scope_of_page_disclosure": "Scope of page disclosure",
   "scope_of_page_disclosure": "Scope of page disclosure",
@@ -163,29 +163,29 @@
   },
   },
   "page_me_apitoken": {
   "page_me_apitoken": {
     "notice": {
     "notice": {
-      "apitoken_issued": "API Token is not issued.",
-      "update_token1": "You can update to generate a new API Token.",
-      "update_token2": "You will need to update the API Token in any existing processes."
+      "apitoken_issued": "API token is not issued.",
+      "update_token1": "You can update to generate a new API token.",
+      "update_token2": "You will need to update the API token in any existing processes."
     },
     },
     "form_help": {}
     "form_help": {}
   },
   },
   "Password": "Password",
   "Password": "Password",
-  "Password Settings": "Password Settings",
+  "Password Settings": "Password settings",
     "personal_settings": {
     "personal_settings": {
     "disassociate_external_account": "Disassociate External Account",
     "disassociate_external_account": "Disassociate External Account",
     "disassociate_external_account_desc": "Are you sure to disassociate the <strong>{{providerType}}</strong> account <strong>{{accountId}}</strong>?",
     "disassociate_external_account_desc": "Are you sure to disassociate the <strong>{{providerType}}</strong> account <strong>{{accountId}}</strong>?",
     "set_new_password": "Set new Password",
     "set_new_password": "Set new Password",
-    "update_password": "Update Password",
+    "update_password": "Update password",
       "current_password": "Current password",
       "current_password": "Current password",
       "new_password": "New password",
       "new_password": "New password",
       "new_password_confirm": "Re-enter new password",
       "new_password_confirm": "Re-enter new password",
       "password_is_not_set": "Password is not set"
       "password_is_not_set": "Password is not set"
     },
     },
-  "security_settings": "Security Settings",
-  "API Settings": "API Settings",
-  "API Token Settings": "API Token Settings",
-  "Current API Token": "Current API Token",
-  "Update API Token": "Update API Token",
+  "security_settings": "Security settings",
+  "API Settings": "API settings",
+  "API Token Settings": "API token settings",
+  "Current API Token": "Current API token",
+  "Update API Token": "Update API token",
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "All pages": "All pages",
       "All pages": "All pages",
@@ -273,15 +273,15 @@
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
     }
     }
   },
   },
-  "Put Back": "Put Back",
-  "Delete Completely": "Delete Completely",
+  "Put Back": "Put back",
+  "Delete Completely": "Delete completely",
   "modal_delete": {
   "modal_delete": {
-    "delete_page": "Delete Page",
-    "deleting_page": "Deleting Page",
+    "delete_page": "Delete page",
+    "deleting_page": "Deleting page",
     "delete_recursively": "Delete child pages recursively.",
     "delete_recursively": "Delete child pages recursively.",
-    "delete_completely": "Delete Completely",
+    "delete_completely": "Delete completely",
     "delete_completely_restriction": "You don't have the authority to delete pages completely.",
     "delete_completely_restriction": "You don't have the authority to delete pages completely.",
-    "recursively": "Delete children of under <code>%s</code> recursively.",
+    "recursively": "Delete children of <code>%s</code> recursively.",
     "completely": "Delete completely instead of putting it into trash."
     "completely": "Delete completely instead of putting it into trash."
   },
   },
   "modal_empty":{
   "modal_empty":{
@@ -296,11 +296,11 @@
   },
   },
   "modal_putback": {
   "modal_putback": {
     "label": {
     "label": {
-      "Put Back Page": "Put Back Page",
-      "recursively": "Put Back recursively"
+      "Put Back Page": "Put back page",
+      "recursively": "Put back recursively"
     },
     },
     "help": {
     "help": {
-      "recursively": "Put Back children of under <code>%s</code> recursively"
+      "recursively": "Put back children of under <code>%s</code> recursively"
     }
     }
   },
   },
   "modal_shortcuts": {
   "modal_shortcuts": {
@@ -336,11 +336,11 @@
   },
   },
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
-      "Create/Edit Template Page": "Create/Edit Template Page",
-      "Create template under": "Create template page under:<br /><code>%s</code>"
+      "Create/Edit Template Page": "Create/Edit template page",
+      "Create template under": "Create template page under:"
     },
     },
     "option_label": {
     "option_label": {
-      "create/edit": "Create/Edit Template page..",
+      "create/edit": "Create/Edit template page..",
       "select": "Select template page type"
       "select": "Select template page type"
     },
     },
     "children": {
     "children": {
@@ -400,33 +400,42 @@
     "someone_editing": "Someone editing this page on HackMD",
     "someone_editing": "Someone editing this page on HackMD",
     "this_page_has_draft": "This page has a draft on HackMD"
     "this_page_has_draft": "This page has a draft on HackMD"
   },
   },
+  "search_result": {
+    "result_meta": "Found \"{{keyword}}\" in {{total}}.",
+    "deletion_mode_btn_lavel": "Select and delete page",
+    "cancel": "Cancel",
+    "delete": "Delete",
+    "check_all": "Check all",
+    "deletion_modal_header": "Delete page",
+    "delete_completely": "Delete completely"
+  },
   "security_setting": {
   "security_setting": {
     "Security settings": "Security settings",
     "Security settings": "Security settings",
-    "Guest Users Access": "Guest Users Access",
+    "Guest Users Access": "Guest users access",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Register limitation": "Register limitation",
     "Register limitation": "Register limitation",
-    "Register limitation desc": "Restricts ways to register new user.",
+    "Register limitation desc": "Restriction of new users' registration",
     "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
     "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
     "users_without_account": "Users without account is not accessible",
     "users_without_account": "Users without account is not accessible",
     "example": "Example",
     "example": "Example",
-    "restrict_emails": "You can restrict registerable e-mail address.",
-    "for_instance": " For instance, if you use growi within a company, you can write ",
-    "only_those": " Only those whose e-mail address including the company address can register.",
+    "restrict_emails": "You can restrict email registration to your wiki by writing an email domain (beginning with @). ",
+    "for_example": " For example, if you would like to restrict registration to users within the growi.org domain, you can write ",
+    "in_this_case": "; in this case, only users within the growi.org domain would be able to register, and all other users would be rejected.",
     "insert_single": "Please insert single e-mail address per line.",
     "insert_single": "Please insert single e-mail address per line.",
-    "page_listing_1": "Page listing/searching<br>restricted by 'Just Me'",
-    "page_listing_1_desc": "Show pages that are restricted by 'Just Me' option when listing/searching",
-    "page_listing_2": "Page listing/searching<br>restricted by User Group",
-    "page_listing_2_desc": "Show pages that are restricted by User Group when listing/searching",
-    "complete_deletion": "Restrict Complete Deletion of Pages",
+    "page_listing_1": "Page listing/searching<br>restricted by 'Only me'",
+    "page_listing_1_desc": "Show pages that are restricted by 'Only me' option when listing/searching",
+    "page_listing_2": "Page listing/searching<br>restricted by User group",
+    "page_listing_2_desc": "Show pages that are restricted by User group when listing/searching",
+    "complete_deletion": "Restrict complete deletion of pages",
     "complete_deletion_explain": "Restricts users who can completely delete pages.",
     "complete_deletion_explain": "Restricts users who can completely delete pages.",
-    "admin_only": "Admin Only",
-    "admin_and_author": "Admin and Author",
+    "admin_only": "Admin only",
+    "admin_and_author": "Admin and author",
     "anyone": "Anyone",
     "anyone": "Anyone",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "Authentication mechanism settings": "Authentication Mechanism Settings",
     "setup_is_not_yet_complete": "Setup is not yet complete",
     "setup_is_not_yet_complete": "Setup is not yet complete",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
     "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
-    "xss_prevent_setting_link": "Go to Markdown settings",
+    "xss_prevent_setting_link": "Go to Markdown Settings",
     "callback_URL": "Callback URL",
     "callback_URL": "Callback URL",
     "providerName": "Provider Name",
     "providerName": "Provider Name",
     "issuerHost": "Issuer Host",
     "issuerHost": "Issuer Host",
@@ -437,7 +446,7 @@
     "updated_general_security_setting": "Succeeded to update security setting",
     "updated_general_security_setting": "Succeeded to update security setting",
     "setup_not_completed_yet": "Setup not completed yet",
     "setup_not_completed_yet": "Setup not completed yet",
     "guest_mode": {
     "guest_mode": {
-      "deny": "Deny (Registered Users Only)",
+      "deny": "Deny (Registered users only)",
       "readonly": "Accept (Guests can read only)"
       "readonly": "Accept (Guests can read only)"
     },
     },
     "registration_mode": {
     "registration_mode": {
@@ -457,10 +466,10 @@
     "Local": {
     "Local": {
       "name": "ID/Password",
       "name": "ID/Password",
       "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}/code> .",
       "note for the only env option": "The LOCAL authentication is limited by the value of environment variable.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}/code> .",
-      "enable_local": "enable ID/Password"
+      "enable_local": "Enable ID/Password"
     },
     },
     "ldap": {
     "ldap": {
-      "enable_ldap": "enable LDAP",
+      "enable_ldap": "Enable LDAP",
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "server_url_detail": "The LDAP URL of the directory service in the format <code>ldap://host:port/DN</code> or <code>ldaps://host:port/DN</code>.",
       "bind_mode": "Binding Mode",
       "bind_mode": "Binding Mode",
       "bind_manager": "Manager Bind",
       "bind_manager": "Manager Bind",
@@ -494,7 +503,7 @@
     },
     },
     "SAML": {
     "SAML": {
       "name": "SAML",
       "name": "SAML",
-      "enable_saml": "enable SAML",
+      "enable_saml": "Enable SAML",
       "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
       "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
       "mapping_detail": "Specification of mappings for {{target}} when creating new users",
       "mapping_detail": "Specification of mappings for {{target}} when creating new users",
@@ -507,18 +516,18 @@
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
     },
     },
     "Basic": {
     "Basic": {
-      "enable_basic": "enable Basic",
+      "enable_basic": "Enable Basic",
       "name": "Basic Authentication",
       "name": "Basic Authentication",
       "desc_1": "Login with <code>username</code> in Authorization header.",
       "desc_1": "Login with <code>username</code> in Authorization header.",
       "desc_2": "User will be automatically generated if not exist.",
       "desc_2": "User will be automatically generated if not exist.",
       "updated_basic": "Succeeded to update Basic setting"
       "updated_basic": "Succeeded to update Basic setting"
     },
     },
     "OAuth": {
     "OAuth": {
-      "enable_oidc": "enable OIDC",
+      "enable_oidc": "Enable OIDC",
       "register": "Register for %s",
       "register": "Register for %s",
       "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
       "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
       "Google": {
       "Google": {
-        "enable_google": "enable Google OAuth",
+        "enable_google": "Enable Google OAuth",
         "name": "Google OAuth",
         "name": "Google OAuth",
         "register_1": "Access {{link}}",
         "register_1": "Access {{link}}",
         "register_2": "Create Project if no projects exist",
         "register_2": "Create Project if no projects exist",
@@ -531,7 +540,7 @@
         "name": "Facebook OAuth"
         "name": "Facebook OAuth"
       },
       },
       "Twitter": {
       "Twitter": {
-        "enable_twitter": "enable Twitter OAuth",
+        "enable_twitter": "Enable Twitter OAuth",
         "name": "Twitter OAuth",
         "name": "Twitter OAuth",
         "register_1": "Access {{link}}",
         "register_1": "Access {{link}}",
         "register_2": "Sign in Twitter",
         "register_2": "Sign in Twitter",
@@ -541,7 +550,7 @@
         "updated_twitter": "Succeeded to update Twitter OAuth setting"
         "updated_twitter": "Succeeded to update Twitter OAuth setting"
       },
       },
       "GitHub": {
       "GitHub": {
-        "enable_github": "enable GitHub OAuth",
+        "enable_github": "Enable GitHub OAuth",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
         "register_1": "Access {{link}}",
         "register_1": "Access {{link}}",
         "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
         "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
@@ -579,36 +588,36 @@
     }
     }
   },
   },
   "notification_setting": {
   "notification_setting": {
-    "slack_incoming_configuration": "Slack Incoming Webhooks Configuration",
-    "prioritize_webhook": "Prioritize Incoming Webhook than Slack App",
+    "slack_incoming_configuration": "Slack Incoming Webhooks configuration",
+    "prioritize_webhook": "Prioritize incoming webhook than Slack App",
     "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
     "prioritize_webhook_desc": "Check this option and GROWI use Incoming Webhooks even if Slack App settings are enabled.",
-    "slack_app_configuration": "Slack App Configuration",
+    "slack_app_configuration": "Slack app configuration",
     "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
     "slack_app_configuration_desc": "This is the way that compatible with Crowi,<br /> but not recommended in GROWI because it is <strong>too complex</strong>.",
     "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
     "use_instead":"Please use Slack Incoming Webhooks Configuration instead.",
     "how_to": {
     "how_to": {
       "header": "How to configure Incoming Webhooks?",
       "header": "How to configure Incoming Webhooks?",
       "workspace": "(At Workspace) Add a hook",
       "workspace": "(At Workspace) Add a hook",
-      "workspace_desc1": "Go to <a href='https: //slack.com/services/new/incoming-webhook'>Incoming Webhooks Configuration page</a>.",
+      "workspace_desc1": "Go to <a href='https: //slack.com/services/new/incoming-webhook'>Incoming Webhooks configuration page</a>.",
       "workspace_desc2": "Choose the default channel to post.",
       "workspace_desc2": "Choose the default channel to post.",
       "workspace_desc3": "Add.",
       "workspace_desc3": "Add.",
       "at_growi": "(At GROWI admin page) Set Webhook URL",
       "at_growi": "(At GROWI admin page) Set Webhook URL",
       "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
       "at_growi_desc": "Input &rdquo;Webhook URL&rdquo; and submit on this page."
     },
     },
-    "user_trigger_notification_header": "Default Notification Settings for Patterns",
+    "user_trigger_notification_header": "Default notification settings for patterns",
     "pattern": "Pattern",
     "pattern": "Pattern",
     "channel": "Channel",
     "channel": "Channel",
     "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
     "pattern_desc": "Path name of wiki. Pattern expression with <code>*</code> can be used.",
     "channel_desc": "Slack channel name. Without <code>#</code>.",
     "channel_desc": "Slack channel name. Without <code>#</code>.",
-    "valid_page": "Enable/Disable Notification",
+    "valid_page": "Enable/disable Notification",
     "link_notification_help": "<strong>The page that is able to be viewed only by those who know the link 'Anyone with the link'</strong> is not notified always.",
     "link_notification_help": "<strong>The page that is able to be viewed only by those who know the link 'Anyone with the link'</strong> is not notified always.",
-    "just_me_notification_help": "<strong>The page that is restricted by 'only me'</strong> is notify when the page edited.",
+    "just_me_notification_help": "<strong>The page that is restricted by 'Only Me'</strong> is notify when the page edited.",
     "group_notification_help": "<strong>The page that is restricted by 'User Group'</strong> is notify when the page edited.",
     "group_notification_help": "<strong>The page that is restricted by 'User Group'</strong> is notify when the page edited.",
-    "notification_list": "List of Notification Settings",
-    "add_notification": "Add New",
-    "trigger_path": "Trigger Path",
+    "notification_list": "List of notification settings",
+    "add_notification": "Add new",
+    "trigger_path": "Trigger path",
     "trigger_path_help": "(expression with <code>*</code> is supported)",
     "trigger_path_help": "(expression with <code>*</code> is supported)",
-    "trigger_events": "Trigger Events",
-    "notify_to": "Notify To",
+    "trigger_events": "Trigger events",
+    "notify_to": "Notify to",
     "back_to_list": "Go back to list",
     "back_to_list": "Go back to list",
     "notification_detail": "Notification Setting Details",
     "notification_detail": "Notification Setting Details",
     "event_pageCreate": "When new page is \"CREATED\"",
     "event_pageCreate": "When new page is \"CREATED\"",
@@ -628,8 +637,8 @@
     "toggle_notification": "Updated setting of {{path}}"
     "toggle_notification": "Updated setting of {{path}}"
   },
   },
   "full_text_search_management": {
   "full_text_search_management": {
-    "elasticsearch_management": "Elasticsearch Management",
-    "connection_status": "Connection Status",
+    "elasticsearch_management": "Elasticsearch management",
+    "connection_status": "Connection status",
     "connection_status_label_unconfigured": "UNCONFIGURED",
     "connection_status_label_unconfigured": "UNCONFIGURED",
     "connection_status_label_connected": "CONNECTED",
     "connection_status_label_connected": "CONNECTED",
     "connection_status_label_disconnected": "DISCONNECTED",
     "connection_status_label_disconnected": "DISCONNECTED",
@@ -637,15 +646,15 @@
     "indices_status": "Indices Status",
     "indices_status": "Indices Status",
     "indices_status_label_normalized": "NORMALIZED",
     "indices_status_label_normalized": "NORMALIZED",
     "indices_status_label_unnormalized": "REBUILDING or BROKEN",
     "indices_status_label_unnormalized": "REBUILDING or BROKEN",
-    "indices_summary": "Indices Summary",
+    "indices_summary": "Indices summary",
     "reconnect": "Reconnect",
     "reconnect": "Reconnect",
-    "reconnect_button": "Try to Reconnect",
+    "reconnect_button": "Try to reconnect",
     "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
     "reconnect_description": "Click the button to try to reconnect to Elasticsearch.",
     "normalize": "Normalize",
     "normalize": "Normalize",
-    "normalize_button": "Normalize Indices",
+    "normalize_button": "Normalize indices",
     "normalize_description": "Click the button to repair broken indices.",
     "normalize_description": "Click the button to repair broken indices.",
     "rebuild": "Rebuild",
     "rebuild": "Rebuild",
-    "rebuild_button": "Rebuild Index",
+    "rebuild_button": "Rebuild index",
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_1": "Click the button to rebuild index and add all page datas.",
     "rebuild_description_2": "This may take a while."
     "rebuild_description_2": "This may take a while."
   },
   },

+ 2 - 2
resource/locales/en-US/welcome.md

@@ -25,5 +25,5 @@ Slack
 
 
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 <a href="https://growi-slackin.weseek.co.jp/"><img src="https://growi-slackin.weseek.co.jp/badge.svg"></a>
 
 
-Please join Slack by all means to make GROWI better.
-In addition to discussing development, we also accept questions at the time of introduction.
+Let's join our Slack channel for all to help make GROWI better.
+In addition to discussing development, we also accept questions at the time of introduction.

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

@@ -76,8 +76,8 @@
     "xss_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
     "xss_desc": "マークダウンテキスト内の HTML タグの扱いを設定し、悪意のあるプログラムからの攻撃を防ぎます",
     "xss_options": {
     "xss_options": {
       "enable_xss_prevention": "XSSを抑制する",
       "enable_xss_prevention": "XSSを抑制する",
-      "ignore_all_tags": "すべてのタグを抑制する",
-      "ignore_all_tags_desc": "すべてのHTMLタグと属性を使用不可にします",
+      "remove_all_tags": "すべてのタグを抑制する",
+      "remove_all_tags_desc": "すべてのHTMLタグと属性を使用不可にします",
       "recommended_setting": "おすすめ設定",
       "recommended_setting": "おすすめ設定",
       "custom_whitelist": "カスタムホワイトリスト",
       "custom_whitelist": "カスタムホワイトリスト",
       "tag_names": "タグ名",
       "tag_names": "タグ名",
@@ -249,6 +249,8 @@
       "new_password": "新しいパスワード"
       "new_password": "新しいパスワード"
     },
     },
     "external_account": "外部アカウントの管理",
     "external_account": "外部アカウントの管理",
+    "external_accounts": "外部アカウント",
+    "create_external_account":"外部アカウントの作成",
     "external_account_list": "外部アカウント一覧",
     "external_account_list": "外部アカウント一覧",
     "invite": "招待する",
     "invite": "招待する",
     "invited": "ユーザーを招待しました",
     "invited": "ユーザーを招待しました",

+ 15 - 6
resource/locales/ja/translation.json

@@ -2,7 +2,7 @@
   "Help": "ヘルプ",
   "Help": "ヘルプ",
   "Edit": "編集",
   "Edit": "編集",
   "Delete": "削除",
   "Delete": "削除",
-  "Delete All": "全て削除",
+  "delete_all": "全て削除",
   "Duplicate": "複製",
   "Duplicate": "複製",
   "Copy": "コピー",
   "Copy": "コピー",
   "Click to copy": "クリックでコピー",
   "Click to copy": "クリックでコピー",
@@ -89,7 +89,7 @@
   "New Page": "新規ページ",
   "New Page": "新規ページ",
   "Create under": "ページを以下に作成",
   "Create under": "ページを以下に作成",
   "Table of Contents": "目次",
   "Table of Contents": "目次",
-  "Management Wiki Home": "Wiki管理トップ",
+  "Wiki Management Home Page": "Wiki管理トップ",
   "App Settings": "アプリ設定",
   "App Settings": "アプリ設定",
   "Site URL settings": "サイトURL設定",
   "Site URL settings": "サイトURL設定",
   "Markdown Settings": "マークダウン設定",
   "Markdown Settings": "マークダウン設定",
@@ -107,7 +107,7 @@
   "Public": "公開",
   "Public": "公開",
   "Anyone with the link": "リンクを知っている人のみ",
   "Anyone with the link": "リンクを知っている人のみ",
   "Specified users": "特定ユーザーのみ",
   "Specified users": "特定ユーザーのみ",
-  "Just me": "自分のみ",
+  "Only me": "自分のみ",
   "Only inside the group": "特定グループのみ",
   "Only inside the group": "特定グループのみ",
   "page_list_and_search_results": "ページリスト・検索結果",
   "page_list_and_search_results": "ページリスト・検索結果",
   "scope_of_page_disclosure": "ページの公開範囲",
   "scope_of_page_disclosure": "ページの公開範囲",
@@ -335,7 +335,7 @@
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
       "Create/Edit Template Page": "テンプレートページの作成/編集",
-      "Create template under": "<code>%s</code><br />にテンプレートページを作成"
+      "Create template under": "以下のパスにテンプレートページを作成"
     },
     },
     "option_label": {
     "option_label": {
       "select": "テンプレートタイプを選択してください",
       "select": "テンプレートタイプを選択してください",
@@ -398,6 +398,15 @@
     "someone_editing": "このページは、HackMD で編集されています。",
     "someone_editing": "このページは、HackMD で編集されています。",
     "this_page_has_draft": "このページは、HackMD のドラフトがあります。"
     "this_page_has_draft": "このページは、HackMD のドラフトがあります。"
   },
   },
+  "search_result": {
+    "result_meta": "{{total}}件のページが見つかりました。検索ワード: \"{{keyword}}\"",
+    "deletion_mode_btn_lavel": "ページを指定して削除",
+    "cancel": "キャンセル",
+    "delete": "削除",
+    "check_all": "すべてチェック",
+    "deletion_modal_header": "以下のページを削除",
+    "delete_completely": "完全に削除する"
+  },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Fixed by env var": "環境変数 <code>{{forcewikimode}}={{wikimode}}</code> により固定されています。",
     "Fixed by env var": "環境変数 <code>{{forcewikimode}}={{wikimode}}</code> により固定されています。",
@@ -407,8 +416,8 @@
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
     "example": "例",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
-    "for_instance": "例えば、",
-    "only_those": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
+    "for_example": "例えば、",
+    "in_this_case": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
     "insert_single": "1行に1メールアドレス入力してください。",
     "insert_single": "1行に1メールアドレス入力してください。",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",

+ 14 - 8
src/client/js/admin.jsx

@@ -5,6 +5,8 @@ import { I18nextProvider } from 'react-i18next';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
+import ErrorBoundary from './components/ErrorBoudary';
+
 import AdminHome from './components/Admin/AdminHome/AdminHome';
 import AdminHome from './components/Admin/AdminHome/AdminHome';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import UserGroupDetailPage from './components/Admin/UserGroupDetail/UserGroupDetailPage';
 import NotificationSetting from './components/Admin/Notification/NotificationSetting';
 import NotificationSetting from './components/Admin/Notification/NotificationSetting';
@@ -99,9 +101,11 @@ Object.keys(componentMappings).forEach((key) => {
   if (elem) {
   if (elem) {
     ReactDOM.render(
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
-        <Provider inject={injectableContainers}>
-          {componentMappings[key]}
-        </Provider>
+        <ErrorBoundary>
+          <Provider inject={injectableContainers}>
+            {componentMappings[key]}
+          </Provider>
+        </ErrorBoundary>
       </I18nextProvider>,
       </I18nextProvider>,
       elem,
       elem,
     );
     );
@@ -124,11 +128,13 @@ if (adminSecuritySettingElem != null) {
     adminOidcSecurityContainer, adminBasicSecurityContainer, adminGoogleSecurityContainer, adminGitHubSecurityContainer, adminTwitterSecurityContainer,
     adminOidcSecurityContainer, adminBasicSecurityContainer, adminGoogleSecurityContainer, adminGitHubSecurityContainer, adminTwitterSecurityContainer,
   ];
   ];
   ReactDOM.render(
   ReactDOM.render(
-    <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
-      <I18nextProvider i18n={i18n}>
-        <SecurityManagement />
-      </I18nextProvider>
-    </Provider>,
+    <I18nextProvider i18n={i18n}>
+      <ErrorBoundary>
+        <Provider inject={[...injectableContainers, ...adminSecurityContainers]}>
+          <SecurityManagement />
+        </Provider>
+      </ErrorBoundary>
+    </I18nextProvider>,
     adminSecuritySettingElem,
     adminSecuritySettingElem,
   );
   );
 }
 }

+ 9 - 4
src/client/js/app.jsx

@@ -5,6 +5,7 @@ import { I18nextProvider } from 'react-i18next';
 
 
 import loggerFactory from '@alias/logger';
 import loggerFactory from '@alias/logger';
 
 
+import ErrorBoundary from './components/ErrorBoudary';
 import SearchPage from './components/SearchPage';
 import SearchPage from './components/SearchPage';
 import TagsList from './components/TagsList';
 import TagsList from './components/TagsList';
 import PageEditor from './components/PageEditor';
 import PageEditor from './components/PageEditor';
@@ -111,9 +112,11 @@ Object.keys(componentMappings).forEach((key) => {
   if (elem) {
   if (elem) {
     ReactDOM.render(
     ReactDOM.render(
       <I18nextProvider i18n={i18n}>
       <I18nextProvider i18n={i18n}>
-        <Provider inject={injectableContainers}>
-          {componentMappings[key]}
-        </Provider>
+        <ErrorBoundary>
+          <Provider inject={injectableContainers}>
+            {componentMappings[key]}
+          </Provider>
+        </ErrorBoundary>
       </I18nextProvider>,
       </I18nextProvider>,
       elem,
       elem,
     );
     );
@@ -124,7 +127,9 @@ Object.keys(componentMappings).forEach((key) => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
 $('a[data-toggle="tab"][href="#revision-history"]').on('show.bs.tab', () => {
   ReactDOM.render(
   ReactDOM.render(
     <I18nextProvider i18n={i18n}>
     <I18nextProvider i18n={i18n}>
-      <PageHistory pageId={pageContainer.state.pageId} crowi={appContainer} />
+      <ErrorBoundary>
+        <PageHistory pageId={pageContainer.state.pageId} crowi={appContainer} />
+      </ErrorBoundary>
     </I18nextProvider>, document.getElementById('revision-history'),
     </I18nextProvider>, document.getElementById('revision-history'),
   );
   );
 });
 });

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

@@ -11,6 +11,8 @@ import StaffCredit from './components/StaffCredit/StaffCredit';
 
 
 import AppContainer from './services/AppContainer';
 import AppContainer from './services/AppContainer';
 import WebsocketContainer from './services/WebsocketContainer';
 import WebsocketContainer from './services/WebsocketContainer';
+import PageCreateButton from './components/Navbar/PageCreateButton';
+import PageCreateModal from './components/PageCreateModal';
 
 
 const logger = loggerFactory('growi:app');
 const logger = loggerFactory('growi:app');
 
 
@@ -44,6 +46,10 @@ const componentMappings = {
   'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
   'search-sidebar': <HeaderSearchBox crowi={appContainer} />,
   'personal-dropdown': <PersonalDropdown />,
   'personal-dropdown': <PersonalDropdown />,
 
 
+  'create-page-button': <PageCreateButton />,
+  'create-page-button-icon': <PageCreateButton isIcon />,
+  'page-create-modal': <PageCreateModal />,
+
   'grw-sidebar-wrapper': <Sidebar />,
   'grw-sidebar-wrapper': <Sidebar />,
 
 
   'staff-credit': <StaffCredit />,
   'staff-credit': <StaffCredit />,

+ 2 - 2
src/client/js/components/Admin/App/AwsSetting.jsx

@@ -103,7 +103,7 @@ class AwsSetting extends React.Component {
 
 
         <div className="row form-group">
         <div className="row form-group">
           <label className="text-left text-md-right col-md-3 col-form-label">
           <label className="text-left text-md-right col-md-3 col-form-label">
-            Access Key ID
+            Access key ID
           </label>
           </label>
           <div className="col-md-6">
           <div className="col-md-6">
             <input
             <input
@@ -119,7 +119,7 @@ class AwsSetting extends React.Component {
 
 
         <div className="row form-group">
         <div className="row form-group">
           <label className="text-left text-md-right col-md-3 col-form-label">
           <label className="text-left text-md-right col-md-3 col-form-label">
-            Secret Access Key
+            Secret access key
           </label>
           </label>
           <div className="col-md-6">
           <div className="col-md-6">
             <input
             <input

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

@@ -20,7 +20,7 @@ class CustomizeLayoutOptions extends React.Component {
             layoutType="crowi-plus"
             layoutType="crowi-plus"
             isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
             isSelected={adminCustomizeContainer.state.currentLayout === 'growi'}
             onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
             onSelected={() => adminCustomizeContainer.switchLayoutType('growi')}
-            labelHtml={`GROWI Enhanced Layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
+            labelHtml={`GROWI enhanced layout <small class="text-success">${t('admin:customize_setting.recommended')}</small>`}
           >
           >
             <h4>{t('admin:customize_setting.layout_desc.growi_title')}</h4>
             <h4>{t('admin:customize_setting.layout_desc.growi_title')}</h4>
             <div className="text-justify d-inline-block">
             <div className="text-justify d-inline-block">
@@ -38,7 +38,7 @@ class CustomizeLayoutOptions extends React.Component {
             layoutType="kibela"
             layoutType="kibela"
             isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
             isSelected={adminCustomizeContainer.state.currentLayout === 'kibela'}
             onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
             onSelected={() => adminCustomizeContainer.switchLayoutType('kibela')}
-            labelHtml="Kibela Like Layout"
+            labelHtml="Kibela like layout"
           >
           >
             <h4>{t('admin:customize_setting.layout_desc.kibela_title')}</h4>
             <h4>{t('admin:customize_setting.layout_desc.kibela_title')}</h4>
             <div className="text-justify d-inline-block">
             <div className="text-justify d-inline-block">

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

@@ -67,7 +67,7 @@ class CustomizeScriptSetting extends React.Component {
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                   <th className="text-right"><code>appContainer</code></th>
                   <th className="text-right"><code>appContainer</code></th>
-                  <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">Unstated Container</a></td>
+                  <td>GROWI App <a href="https://github.com/jamiebuilds/unstated">unstated container</a></td>
                 </tr>
                 </tr>
                 <tr>
                 <tr>
                   <th className="text-right"><code>growiRenderer</code></th>
                   <th className="text-right"><code>growiRenderer</code></th>

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

@@ -56,7 +56,7 @@ class CustomizeTitle extends React.Component {
           <div className="form-text text-muted col-12">
           <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;page&#125;&#125; - &#123;&#123;sitename&#125;&#125;</code>
             <br />
             <br />
-            Default Output: <pre><code className="xml">&lt;title&gt;/Sandbox - {'GROWI'}&lt;&#047;title&gt;</code></pre>
+            Default Output: <code className="xml">&lt;title&gt;/Somewhere/Page - {'GROWI'}&lt;&#047;title&gt;</code>
           </div>
           </div>
           <div className="form-group col-12">
           <div className="form-group col-12">
             <input
             <input

+ 2 - 2
src/client/js/components/Admin/ImportData/GrowiArchive/ImportCollectionConfigurationModal.jsx

@@ -105,10 +105,10 @@ class ImportCollectionConfigurationModal extends React.Component {
             onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
             onChange={() => this.changeHandler({ makePublicForGrant4: !option.makePublicForGrant4 })}
           />
           />
           <label htmlFor="cbOpt2" className="custom-control-label">
           <label htmlFor="cbOpt2" className="custom-control-label">
-            {t(`${translationBase}.set_public_to_page.label`, { from: t('Just me') })}
+            {t(`${translationBase}.set_public_to_page.label`, { from: t('Only me') })}
             <p
             <p
               className="form-text text-muted mt-0"
               className="form-text text-muted mt-0"
-              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Just me') }) }}
+              dangerouslySetInnerHTML={{ __html: t(`${translationBase}.set_public_to_page.desc`, { from: t('Only me') }) }}
             />
             />
           </label>
           </label>
         </div>
         </div>

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

@@ -127,7 +127,7 @@ class ImportForm extends React.Component {
         isImported: true,
         isImported: true,
       });
       });
 
 
-      toastSuccess(undefined, 'Import process has terminated.');
+      toastSuccess(undefined, 'Import process has completed.');
     });
     });
 
 
     // websocket event
     // websocket event

+ 2 - 2
src/client/js/components/Admin/MarkdownSetting/XssForm.jsx

@@ -55,9 +55,9 @@ class XssForm extends React.Component {
                 onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
                 onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
               />
               />
               <label className="custom-control-label w-100" htmlFor="xssOption1">
               <label className="custom-control-label w-100" htmlFor="xssOption1">
-                <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.ignore_all_tags')}</p>
+                <p className="font-weight-bold">{t('admin:markdown_setting.xss_options.remove_all_tags')}</p>
                 <div className="mt-4">
                 <div className="mt-4">
-                  {t('admin:markdown_setting.xss_options.ignore_all_tags_desc')}
+                  {t('admin:markdown_setting.xss_options.remove_all_tags_desc')}
                 </div>
                 </div>
               </label>
               </label>
             </div>
             </div>

+ 3 - 3
src/client/js/components/Admin/Notification/NotificationSetting.jsx

@@ -65,7 +65,7 @@ class NotificationSetting extends React.Component {
               onClick={() => { this.toggleActiveTab('slack-configuration') }}
               onClick={() => { this.toggleActiveTab('slack-configuration') }}
               href="#slack-configuration"
               href="#slack-configuration"
             >
             >
-              <i className="icon-settings"></i> Slack Configuration
+              <i className="icon-settings"></i> Slack configuration
             </NavLink>
             </NavLink>
           </NavItem>
           </NavItem>
           <NavItem>
           <NavItem>
@@ -74,7 +74,7 @@ class NotificationSetting extends React.Component {
               onClick={() => { this.toggleActiveTab('user-trigger-notification') }}
               onClick={() => { this.toggleActiveTab('user-trigger-notification') }}
               href="#user-trigger-notification"
               href="#user-trigger-notification"
             >
             >
-              <i className="icon-settings"></i> User Trigger Notification
+              <i className="icon-settings"></i> User trigger notification
             </NavLink>
             </NavLink>
           </NavItem>
           </NavItem>
           <NavItem>
           <NavItem>
@@ -83,7 +83,7 @@ class NotificationSetting extends React.Component {
               onClick={() => { this.toggleActiveTab('global-notification') }}
               onClick={() => { this.toggleActiveTab('global-notification') }}
               href="#global-notification"
               href="#global-notification"
             >
             >
-              <i className="icon-settings"></i> Global Notification
+              <i className="icon-settings"></i> Global notification
             </NavLink>
             </NavLink>
           </NavItem>
           </NavItem>
         </Nav>
         </Nav>

+ 1 - 1
src/client/js/components/Admin/Notification/SlackAppConfiguration.jsx

@@ -116,7 +116,7 @@ class SlackAppConfiguration extends React.Component {
               </div>
               </div>
 
 
               <div className="row mb-5">
               <div className="row mb-5">
-                <label className="col-md-3 text-left text-md-right">OAuth Access Token</label>
+                <label className="col-md-3 text-left text-md-right">OAuth access token</label>
                 <div className="col-md-6">
                 <div className="col-md-6">
                   <input
                   <input
                     className="form-control"
                     className="form-control"

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

@@ -147,8 +147,8 @@ class LocalSecuritySetting extends React.Component {
                   defaultValue={adminLocalSecurityContainer.state.registrationWhiteList.join('\n')}
                   defaultValue={adminLocalSecurityContainer.state.registrationWhiteList.join('\n')}
                   onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
                   onChange={e => adminLocalSecurityContainer.changeRegistrationWhiteList(e.target.value)}
                 />
                 />
-                <p className="form-text text-muted small">{t('security_setting.restrict_emails')}<br />{t('security_setting.for_instance')}
-                  <code>@growi.org</code>{t('security_setting.only_those')}<br />
+                <p className="form-text text-muted small">{t('security_setting.restrict_emails')}<br />{t('security_setting.for_example')}
+                  <code>@growi.org</code>{t('security_setting.in_this_case')}<br />
                   {t('security_setting.insert_single')}
                   {t('security_setting.insert_single')}
                 </p>
                 </p>
               </div>
               </div>

+ 2 - 2
src/client/js/components/Admin/UserGroupDetail/UserGroupUserModal.jsx

@@ -31,14 +31,14 @@ class UserGroupUserModal extends React.Component {
             <div className="col-6">
             <div className="col-6">
               <div className="mb-5">
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                 <CheckBoxForSerchUserOption
-                  option="Mail"
+                  option="mail"
                   checked={adminUserGroupDetailContainer.state.isAlsoMailSearched}
                   checked={adminUserGroupDetailContainer.state.isAlsoMailSearched}
                   onChange={adminUserGroupDetailContainer.switchIsAlsoMailSearched}
                   onChange={adminUserGroupDetailContainer.switchIsAlsoMailSearched}
                 />
                 />
               </div>
               </div>
               <div className="mb-5">
               <div className="mb-5">
                 <CheckBoxForSerchUserOption
                 <CheckBoxForSerchUserOption
-                  option="Name"
+                  option="name"
                   checked={adminUserGroupDetailContainer.state.isAlsoNameSearched}
                   checked={adminUserGroupDetailContainer.state.isAlsoNameSearched}
                   onChange={adminUserGroupDetailContainer.switchIsAlsoNameSearched}
                   onChange={adminUserGroupDetailContainer.switchIsAlsoNameSearched}
                 />
                 />

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

@@ -89,7 +89,7 @@ class UserTable extends React.Component {
 
 
     return (
     return (
       <Fragment>
       <Fragment>
-        <div className="table-responsive text-nowrap">
+        <div className="table-responsive text-nowrap h-100">
           <table className="table table-default table-bordered table-user-list">
           <table className="table table-default table-bordered table-user-list">
             <thead>
             <thead>
               <tr>
               <tr>

+ 56 - 0
src/client/js/components/ErrorBoudary.jsx

@@ -0,0 +1,56 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+/**
+ * @see https://reactjs.org/docs/error-boundaries.html
+ */
+class ErrorBoundary extends React.Component {
+
+  constructor(props) {
+    super(props);
+    this.state = { error: null, errorInfo: null };
+  }
+
+  componentDidCatch(error, errorInfo) {
+    // You can also log the error to an error reporting service
+    // logErrorToMyService(error, errorInfo);
+
+    // Catch errors in any components below and re-render with error message
+    this.setState({
+      error,
+      errorInfo,
+    });
+  }
+
+  render() {
+    const { error, errorInfo } = this.state;
+    if (errorInfo != null) {
+
+      // split componetStack
+      // see https://regex101.com/r/Uc448G/1
+      const firstStack = errorInfo.componentStack.split(/\s*in\s/)[1];
+
+      return (
+        <div className="card border-danger">
+          <div className="card-header">Error occured in {firstStack}</div>
+          <div className="card-body">
+            <h5>{error && error.toString()}</h5>
+            <details className="card well small mb-0" style={{ whiteSpace: 'pre-wrap' }}>
+              {errorInfo.componentStack}
+            </details>
+          </div>
+        </div>
+      );
+    }
+
+    // Normally, just render children
+    return this.props.children;
+  }
+
+}
+
+ErrorBoundary.propTypes = {
+  children: PropTypes.object,
+};
+
+export default ErrorBoundary;

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

@@ -0,0 +1,31 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { format, formatDistanceStrict } from 'date-fns';
+import { UncontrolledTooltip } from 'reactstrap';
+
+const FormattedDistanceDate = (props) => {
+
+  // cast to date if string
+  const date = (typeof props.date === 'string') ? new Date(props.date) : props.date;
+
+  const baseDate = props.baseDate || new Date();
+
+  const elemId = `grw-fdd-${props.id}`;
+  const dateFormatted = format(date, 'yyyy/MM/dd HH:mm');
+
+  return (
+    <>
+      <span id={elemId}>{formatDistanceStrict(date, baseDate)}</span>
+      <UncontrolledTooltip placement="bottom" fade={false} target={elemId}>{dateFormatted}</UncontrolledTooltip>
+    </>
+  );
+};
+
+FormattedDistanceDate.propTypes = {
+  id: PropTypes.string.isRequired,
+  date: PropTypes.oneOfType([PropTypes.string, PropTypes.instanceOf(Date)]).isRequired,
+  baseDate: PropTypes.instanceOf(Date),
+};
+
+export default FormattedDistanceDate;

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

@@ -72,7 +72,7 @@ class AssociateModal extends React.Component {
     return (
     return (
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
       <Modal isOpen={this.props.isOpen} toggle={this.props.onClose} size="lg">
         <ModalHeader className="bg-info text-light" toggle={this.props.onClose}>
         <ModalHeader className="bg-info text-light" toggle={this.props.onClose}>
-          { t('Create External Account') }
+          { t('admin:user_management.create_external_account') }
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>
           <ul className="nav nav-tabs passport-settings mb-2" role="tablist">
           <ul className="nav nav-tabs passport-settings mb-2" role="tablist">

+ 2 - 2
src/client/js/components/Me/ExternalAccountLinkedMe.jsx

@@ -73,14 +73,14 @@ class ExternalAccountLinkedMe extends React.Component {
               <i className="icon-plus" aria-hidden="true" />
               <i className="icon-plus" aria-hidden="true" />
             Add
             Add
             </button>
             </button>
-            { t('External Accounts') }
+            { t('admin:user_management.external_accounts') }
           </h2>
           </h2>
         </div>
         </div>
 
 
         <table className="table table-bordered table-user-list">
         <table className="table table-bordered table-user-list">
           <thead>
           <thead>
             <tr>
             <tr>
-              <th width="120px">Authentication Provider</th>
+              <th width="120px">{ t('admin:user_management.authentication_provider') }</th>
               <th>
               <th>
                 <code>accountId</code>
                 <code>accountId</code>
               </th>
               </th>

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

@@ -23,7 +23,10 @@ class PersonalSettings extends React.Component {
                 <a className="nav-link active" href="#user-settings" data-toggle="tab" role="tab"><i className="icon-user"></i> { t('User Information') }</a>
                 <a className="nav-link active" href="#user-settings" data-toggle="tab" role="tab"><i className="icon-user"></i> { t('User Information') }</a>
               </li>
               </li>
               <li className="nav-item">
               <li className="nav-item">
-                <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab"><i className="icon-share-alt"></i> { t('External Accounts') }</a>
+                <a className="nav-link" href="#external-accounts" data-toggle="tab" role="tab">
+                  <i className="icon-share-alt"></i>
+                  { t('admin:user_management.external_accounts') }
+                </a>
               </li>
               </li>
               <li className="nav-item">
               <li className="nav-item">
                 <a className="nav-link" href="#password-settings" data-toggle="tab" role="tab"><i className="icon-lock"></i> { t('Password Settings') }</a>
                 <a className="nav-link" href="#password-settings" data-toggle="tab" role="tab"><i className="icon-lock"></i> { t('Password Settings') }</a>

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

@@ -143,7 +143,7 @@ class MyDraftList extends React.Component {
               <div className="align-self-center">
               <div className="align-self-center">
                 <button type="button" className="btn btn-sm btn-outline-danger" onClick={this.clearAllDrafts}>
                 <button type="button" className="btn btn-sm btn-outline-danger" onClick={this.clearAllDrafts}>
                   <i className="icon-fw icon-fire"></i>
                   <i className="icon-fw icon-fire"></i>
-                  {t('Delete All')}
+                  {t('delete_all')}
                 </button>
                 </button>
               </div>
               </div>
             </div>
             </div>

+ 43 - 0
src/client/js/components/Navbar/PageCreateButton.jsx

@@ -0,0 +1,43 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+
+import { withTranslation } from 'react-i18next';
+
+import { createSubscribedElement } from '../UnstatedUtils';
+import AppContainer from '../../services/AppContainer';
+
+const PageCreateButton = (props) => {
+  const { t, appContainer, isIcon } = props;
+
+  if (isIcon) {
+    return (
+      <button className="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" onClick={appContainer.openPageCreateModal}>
+        <i className="icon-pencil"></i>
+      </button>
+    );
+  }
+
+  return (
+    <a className="nav-link create-page" type="button" onClick={appContainer.openPageCreateModal}>
+      <i className="icon-pencil mr-2"></i>
+      <span>{ t('New') }</span>
+    </a>
+  );
+};
+
+/**
+ * Wrapper component for using unstated
+ */
+const PageCreateButtonWrapper = (props) => {
+  return createSubscribedElement(PageCreateButton, props, [AppContainer]);
+};
+
+
+PageCreateButton.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+
+  isIcon: PropTypes.bool,
+};
+
+export default withTranslation()(PageCreateButtonWrapper);

+ 6 - 6
src/client/js/components/PageComment/Comment.jsx

@@ -1,7 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
-import { format, formatDistanceStrict } from 'date-fns';
+import { format } from 'date-fns';
 
 
 import { UncontrolledTooltip } from 'reactstrap';
 import { UncontrolledTooltip } from 'reactstrap';
 
 
@@ -9,6 +9,8 @@ import AppContainer from '../../services/AppContainer';
 import PageContainer from '../../services/PageContainer';
 import PageContainer from '../../services/PageContainer';
 
 
 import { createSubscribedElement } from '../UnstatedUtils';
 import { createSubscribedElement } from '../UnstatedUtils';
+
+import FormattedDistanceDate from '../FormattedDistanceDate';
 import RevisionBody from '../Page/RevisionBody';
 import RevisionBody from '../Page/RevisionBody';
 import UserPicture from '../User/UserPicture';
 import UserPicture from '../User/UserPicture';
 import Username from '../User/Username';
 import Username from '../User/Username';
@@ -175,9 +177,6 @@ class Comment extends React.PureComponent {
     const revFirst8Letters = comment.revision.substr(-8);
     const revFirst8Letters = comment.revision.substr(-8);
     const revisionLavelClassName = this.getRevisionLabelClassName();
     const revisionLavelClassName = this.getRevisionLabelClassName();
 
 
-    const commentedDateId = `commentDate-${comment._id}`;
-    const commentedDate = <span id={commentedDateId}>{formatDistanceStrict(createdAt, new Date())}</span>;
-    const commentedDateFormatted = format(createdAt, 'yyyy/MM/dd HH:mm');
     const editedDateId = `editedDate-${comment._id}`;
     const editedDateId = `editedDate-${comment._id}`;
     const editedDateFormatted = isEdited
     const editedDateFormatted = isEdited
       ? format(updatedAt, 'yyyy/MM/dd HH:mm')
       ? format(updatedAt, 'yyyy/MM/dd HH:mm')
@@ -206,8 +205,9 @@ class Comment extends React.PureComponent {
               </div>
               </div>
               <div className="page-comment-body">{commentBody}</div>
               <div className="page-comment-body">{commentBody}</div>
               <div className="page-comment-meta">
               <div className="page-comment-meta">
-                <span><a href={`#${commentId}`}>{commentedDate}</a></span>
-                <UncontrolledTooltip placement="bottom" fade={false} target={commentedDateId}>{commentedDateFormatted}</UncontrolledTooltip>
+                <a href={`#${commentId}`}>
+                  <FormattedDistanceDate id={commentId} date={comment.createdAt} />
+                </a>
                 { isEdited && (
                 { isEdited && (
                   <>
                   <>
                     <span id={editedDateId}>&nbsp;(edited)</span>
                     <span id={editedDateId}>&nbsp;(edited)</span>

+ 229 - 0
src/client/js/components/PageCreateModal.jsx

@@ -0,0 +1,229 @@
+
+import React, { useState } from 'react';
+import PropTypes from 'prop-types';
+
+import { Modal, ModalHeader, ModalBody } from 'reactstrap';
+
+import { withTranslation } from 'react-i18next';
+import { format } from 'date-fns';
+import urljoin from 'url-join';
+
+import { userPageRoot } from '@commons/util/path-utils';
+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 config = appContainer.getConfig();
+  const isReachable = config.isSearchServiceReachable;
+  const { path } = pageContainer.state;
+  const userPageRootPath = userPageRoot(appContainer.currentUser);
+  const parentPath = pathUtils.addTrailingSlash(path);
+  const now = format(new Date(), 'yyyy/MM/dd');
+
+  const [todayInput1, setTodayInput1] = useState(t('Memo'));
+  const [todayInput2, setTodayInput2] = useState('');
+  const [pageNameInput, setPageNameInput] = useState(parentPath);
+  const [template, setTemplate] = useState(null);
+
+  /**
+   * change todayInput1
+   * @param {string} value
+   */
+  function onChangeTodayInput1Handler(value) {
+    setTodayInput1(value);
+  }
+
+  /**
+   * change todayInput2
+   * @param {string} value
+   */
+  function onChangeTodayInput2Handler(value) {
+    setTodayInput2(value);
+  }
+
+  /**
+   * change pageNameInput
+   * @param {string} value
+   */
+  function onChangePageNameInputHandler(value) {
+    setPageNameInput(value);
+  }
+
+  /**
+   * change template
+   * @param {string} value
+   */
+  function onChangeTemplateHandler(value) {
+    setTemplate(value);
+  }
+
+  /**
+   * access today page
+   */
+  function createTodayPage() {
+    let tmpTodayInput1 = todayInput1;
+    if (tmpTodayInput1 === '') {
+      tmpTodayInput1 = t('Memo');
+    }
+    window.location.href = encodeURI(urljoin(userPageRootPath, tmpTodayInput1, now, todayInput2, '#edit'));
+  }
+
+  /**
+   * access input page
+   */
+  function createInputPage() {
+    window.location.href = encodeURI(urljoin(pageNameInput, '#edit'));
+  }
+
+  /**
+   * access template page
+   */
+  function createTemplatePage() {
+    const pageName = (template === 'children') ? '_template' : '__template';
+    window.location.href = encodeURI(urljoin(parentPath, pageName, '#edit'));
+  }
+
+  function renderCreateTodayForm() {
+    return (
+      <div className="row form-group">
+        <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>
+              <input
+                type="text"
+                className="page-today-input2 form-control"
+                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}>
+                <i className="icon-fw icon-doc"></i>{ t('Create') }
+              </button>
+            </div>
+          </div>
+        </fieldset>
+      </div>
+    );
+  }
+
+  function renderInputPageForm() {
+    return (
+      <div className="row form-group">
+        <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">
+              {isReachable
+                // GW-2355 refactor typeahead
+                ? <PagePathAutoComplete crowi={appContainer} initializedPath={path} addTrailingSlash />
+                : (
+                  <input
+                    type="text"
+                    value={pageNameInput}
+                    className="page-name-input form-control"
+                    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}>
+                <i className="icon-fw icon-doc"></i>{ t('Create') }
+              </button>
+            </div>
+          </div>
+        </fieldset>
+      </div>
+    );
+  }
+
+  function renderTemplatePageForm() {
+    return (
+      <div className="row form-group">
+        <fieldset className="col-12">
+          <h3 className="grw-modal-head pb-2">{ t('template.modal_label.Create template under')}<br />
+            <code>{path}</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>
+            <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>
+              </button>
+            </div>
+          </div>
+        </fieldset>
+      </div>
+    );
+  }
+  return (
+    <Modal size="lg" isOpen={appContainer.state.isPageCreateModalShown} toggle={appContainer.closePageCreateModal}>
+      <ModalHeader tag="h4" toggle={appContainer.closePageCreateModal} className="bg-primary text-light">
+        { t('New Page') }
+      </ModalHeader>
+      <ModalBody>
+        {renderCreateTodayForm}
+        {renderInputPageForm}
+        {renderTemplatePageForm}
+      </ModalBody>
+    </Modal>
+
+  );
+};
+
+
+/**
+ * Wrapper component for using unstated
+ */
+const ModalControlWrapper = (props) => {
+  return createSubscribedElement(PageCreateModal, props, [AppContainer, PageContainer]);
+};
+
+
+PageCreateModal.propTypes = {
+  t: PropTypes.func.isRequired, //  i18next
+  appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
+};
+
+export default withTranslation()(ModalControlWrapper);

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

@@ -261,7 +261,7 @@ export default class Editor extends AbstractEditor {
     return (
     return (
       <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
       <Modal isOpen={this.state.isCheatsheetModalShown} toggle={hideCheatsheetModal} className="modal-gfm-cheatsheet">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
         <ModalHeader tag="h4" toggle={hideCheatsheetModal} className="bg-primary text-light">
-          <i className="icon-fw icon-question" />Markdown Help
+          <i className="icon-fw icon-question" />Markdown help
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>
           <Cheatsheet />
           <Cheatsheet />

+ 1 - 1
src/client/js/components/SavePageControls/GrantSelector.jsx

@@ -37,7 +37,7 @@ class GrantSelector extends React.Component {
       },
       },
       // { grant: 3, iconClass: '', label: 'Specified users only' },
       // { grant: 3, iconClass: '', label: 'Specified users only' },
       {
       {
-        grant: 4, iconClass: 'icon-lock', styleClass: 'text-danger', label: 'Just me',
+        grant: 4, iconClass: 'icon-lock', styleClass: 'text-danger', label: 'Only me',
       },
       },
       {
       {
         grant: 5, iconClass: 'icon-options', styleClass: '', label: 'Only inside the group', reselectLabel: 'Reselect the group',
         grant: 5, iconClass: 'icon-options', styleClass: '', label: 'Only inside the group', reselectLabel: 'Reselect the group',

+ 12 - 4
src/client/js/components/SearchPage/DeletePageListModal.jsx

@@ -1,12 +1,14 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 
 
+import { withTranslation } from 'react-i18next';
+
 import {
 import {
   Button,
   Button,
   Modal, ModalHeader, ModalBody, ModalFooter,
   Modal, ModalHeader, ModalBody, ModalFooter,
 } from 'reactstrap';
 } from 'reactstrap';
 
 
-export default class DeletePageListModal extends React.Component {
+class DeletePageListModal extends React.Component {
 
 
   /*
   /*
    * the threshold for omitting body
    * the threshold for omitting body
@@ -17,6 +19,7 @@ export default class DeletePageListModal extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { t } = this.props;
     if (this.props.pages == null || this.props.pages.length === 0) {
     if (this.props.pages == null || this.props.pages.length === 0) {
       return <div></div>;
       return <div></div>;
     }
     }
@@ -30,7 +33,7 @@ export default class DeletePageListModal extends React.Component {
     return (
     return (
       <Modal isOpen={this.props.isShown} toggle={this.props.cancel} className="page-list-delete-modal">
       <Modal isOpen={this.props.isShown} toggle={this.props.cancel} className="page-list-delete-modal">
         <ModalHeader tag="h4" toggle={this.props.cancel} className="bg-danger text-light">
         <ModalHeader tag="h4" toggle={this.props.cancel} className="bg-danger text-light">
-          Deleting pages:
+          {t('search_result.deletion_modal_header')}
         </ModalHeader>
         </ModalHeader>
         <ModalBody>
         <ModalBody>
           <ul>
           <ul>
@@ -53,11 +56,12 @@ export default class DeletePageListModal extends React.Component {
                   className="custom-control-label text-danger"
                   className="custom-control-label text-danger"
                   htmlFor="customCheck-delete-completely"
                   htmlFor="customCheck-delete-completely"
                 >
                 >
-                  Delete completely
+                  {t('search_result.delete_completely')}
                 </label>
                 </label>
               </div>
               </div>
               <Button color={this.props.isDeleteCompletely ? 'danger' : 'light'} onClick={this.props.confirmedToDelete}>
               <Button color={this.props.isDeleteCompletely ? 'danger' : 'light'} onClick={this.props.confirmedToDelete}>
-                <i className="icon-trash"></i>Delete
+                <i className="icon-trash"></i>
+                {t('search_result.delete')}
               </Button>
               </Button>
             </span>
             </span>
           </div>
           </div>
@@ -73,6 +77,8 @@ DeletePageListModal.defaultProps = {
 };
 };
 
 
 DeletePageListModal.propTypes = {
 DeletePageListModal.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
   isShown: PropTypes.bool.isRequired,
   isShown: PropTypes.bool.isRequired,
   pages: PropTypes.array,
   pages: PropTypes.array,
   errorMessage: PropTypes.string,
   errorMessage: PropTypes.string,
@@ -81,3 +87,5 @@ DeletePageListModal.propTypes = {
   confirmedToDelete: PropTypes.func.isRequired, //      for confirmed event handling
   confirmedToDelete: PropTypes.func.isRequired, //      for confirmed event handling
   toggleDeleteCompletely: PropTypes.func.isRequired, // for delete completely check event handling
   toggleDeleteCompletely: PropTypes.func.isRequired, // for delete completely check event handling
 };
 };
+
+export default withTranslation()(DeletePageListModal);

+ 10 - 5
src/client/js/components/SearchPage/SearchResult.jsx

@@ -2,6 +2,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import * as toastr from 'toastr';
 import * as toastr from 'toastr';
 
 
+import { withTranslation } from 'react-i18next';
+
 import Page from '../PageList/Page';
 import Page from '../PageList/Page';
 import SearchResultList from './SearchResultList';
 import SearchResultList from './SearchResultList';
 import DeletePageListModal from './DeletePageListModal';
 import DeletePageListModal from './DeletePageListModal';
@@ -205,6 +207,8 @@ class SearchResult extends React.Component {
   }
   }
 
 
   render() {
   render() {
+    const { t } = this.props;
+
     if (this.isError()) {
     if (this.isError()) {
       return (
       return (
         <div className="content-main">
         <div className="content-main">
@@ -237,7 +241,7 @@ class SearchResult extends React.Component {
       deletionModeButtons = (
       deletionModeButtons = (
         <div className="btn-group">
         <div className="btn-group">
           <button type="button" className="btn btn-outline-secondary btn-sm rounded-pill-weak" onClick={() => { return this.handleDeletionModeChange() }}>
           <button type="button" className="btn btn-outline-secondary btn-sm rounded-pill-weak" onClick={() => { return this.handleDeletionModeChange() }}>
-            <i className="icon-ban" /> Cancel
+            <i className="icon-ban" /> {t('search_result.cancel')}
           </button>
           </button>
           <button
           <button
             type="button"
             type="button"
@@ -245,7 +249,7 @@ class SearchResult extends React.Component {
             onClick={() => { return this.showDeleteConfirmModal() }}
             onClick={() => { return this.showDeleteConfirmModal() }}
             disabled={this.state.selectedPages.size === 0}
             disabled={this.state.selectedPages.size === 0}
           >
           >
-            <i className="icon-trash" /> Delete
+            <i className="icon-trash" /> {t('search_result.delete')}
           </button>
           </button>
         </div>
         </div>
       );
       );
@@ -258,7 +262,7 @@ class SearchResult extends React.Component {
             onChange={() => { return this.handleAllSelect() }}
             onChange={() => { return this.handleAllSelect() }}
             checked={this.isAllSelected()}
             checked={this.isAllSelected()}
           />
           />
-          <label className="custom-control-label" htmlFor="all-select-check">&nbsp;Check All</label>
+          <label className="custom-control-label" htmlFor="all-select-check">&nbsp;{t('search_result.check_all')}</label>
         </div>
         </div>
       );
       );
     }
     }
@@ -266,7 +270,7 @@ class SearchResult extends React.Component {
       deletionModeButtons = (
       deletionModeButtons = (
         <div className="btn-group">
         <div className="btn-group">
           <button type="button" className="btn btn-outline-secondary rounded-pill btn-sm" onClick={() => { return this.handleDeletionModeChange() }}>
           <button type="button" className="btn btn-outline-secondary rounded-pill btn-sm" onClick={() => { return this.handleDeletionModeChange() }}>
-            <i className="ti-check-box" /> DeletionMode
+            <i className="ti-check-box" /> {t('search_result.deletion_mode_btn_lavel')}
           </button>
           </button>
         </div>
         </div>
       );
       );
@@ -326,6 +330,7 @@ const SearchResultWrapper = (props) => {
 
 
 SearchResult.propTypes = {
 SearchResult.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
+  t: PropTypes.func.isRequired, // i18next
 
 
   pages: PropTypes.array.isRequired,
   pages: PropTypes.array.isRequired,
   searchingKeyword: PropTypes.string.isRequired,
   searchingKeyword: PropTypes.string.isRequired,
@@ -337,4 +342,4 @@ SearchResult.defaultProps = {
   searchError: null,
   searchError: null,
 };
 };
 
 
-export default SearchResultWrapper;
+export default withTranslation()(SearchResultWrapper);

+ 76 - 11
src/client/js/components/Sidebar/History.jsx

@@ -1,5 +1,5 @@
 import React from 'react';
 import React from 'react';
-// import PropTypes from 'prop-types';
+import PropTypes from 'prop-types';
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
@@ -8,39 +8,104 @@ import {
   MenuSection,
   MenuSection,
 } from '@atlaskit/navigation-next';
 } from '@atlaskit/navigation-next';
 
 
+import loggerFactory from '@alias/logger';
+
+import DevidedPagePath from '@commons/models/devided-page-path';
+import LinkedPagePath from '@commons/models/linked-page-path';
+import PagePathHierarchicalLink from '@commons/components/PagePathHierarchicalLink';
+
 import { createSubscribedElement } from '../UnstatedUtils';
 import { createSubscribedElement } from '../UnstatedUtils';
 import AppContainer from '../../services/AppContainer';
 import AppContainer from '../../services/AppContainer';
+import { toastError } from '../../util/apiNotification';
+
+import FormattedDistanceDate from '../FormattedDistanceDate';
+import UserPicture from '../User/UserPicture';
 
 
+const logger = loggerFactory('growi:History');
 class History extends React.Component {
 class History extends React.Component {
 
 
   static propTypes = {
   static propTypes = {
+    t: PropTypes.func.isRequired, // i18next
+    appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   };
   };
 
 
-  state = {
-  };
+  constructor(props) {
+    super(props);
+
+    this.reloadData = this.reloadData.bind(this);
+  }
+
+  async componentDidMount() {
+    this.reloadData();
+  }
+
+  async reloadData() {
+    const { appContainer } = this.props;
 
 
-  renderHeaderWordmark() {
-    return <h3>History</h3>;
+    try {
+      await appContainer.retrieveRecentlyUpdated();
+    }
+    catch (error) {
+      logger.error('failed to save', error);
+      toastError(error, 'Error occurred in updating History');
+    }
+  }
+
+  PageItem = ({ page }) => {
+    const dPagePath = new DevidedPagePath(page.path, false, true);
+    const linkedPagePathFormer = new LinkedPagePath(dPagePath.former);
+    const linkedPagePathLatter = new LinkedPagePath(dPagePath.latter);
+    const FormerLink = () => (
+      <div className="grw-page-path-text-muted-container small">
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePathFormer} />
+      </div>
+    );
+
+    return (
+      <li className="list-group-item">
+        <div className="d-flex w-100">
+          <UserPicture user={page.lastUpdatedUser} size="md" />
+          <div className="flex-grow-1 ml-2">
+            { !dPagePath.isRoot && <FormerLink /> }
+            <h4 className="mb-1">
+              <PagePathHierarchicalLink linkedPagePath={linkedPagePathLatter} basePath={dPagePath.isRoot ? undefined : dPagePath.former} />
+            </h4>
+            <div className="text-right small">
+              <FormattedDistanceDate id={page.id} date={page.updatedAt} />
+            </div>
+          </div>
+        </div>
+      </li>
+    );
   }
   }
 
 
   render() {
   render() {
+    const { PageItem } = this;
+    const { t } = this.props;
+    const { recentlyUpdatedPages } = this.props.appContainer.state;
+
     return (
     return (
-      <>
+      <div className="grw-sidebar-history">
         <HeaderSection>
         <HeaderSection>
           { () => (
           { () => (
-            <div className="grw-sidebar-header-container">
-              {this.renderHeaderWordmark()}
+            <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}>
+                <i className="icon icon-reload"></i>
+              </button>
             </div>
             </div>
           ) }
           ) }
         </HeaderSection>
         </HeaderSection>
         <MenuSection>
         <MenuSection>
           { () => (
           { () => (
-            <div className="grw-sidebar-content-container">
-              <span>(TBD) History Contents</span>
+            <div className="grw-sidebar-content-container p-3">
+              <ul className="list-group list-group-flush">
+                { recentlyUpdatedPages.map(page => <PageItem key={page.id} page={page} />) }
+              </ul>
             </div>
             </div>
           ) }
           ) }
         </MenuSection>
         </MenuSection>
-      </>
+      </div>
     );
     );
   }
   }
 
 

+ 1 - 1
src/client/js/components/StaffCredit/Contributor.js

@@ -111,7 +111,7 @@ const contributors = [
         ],
         ],
       },
       },
       {
       {
-        additionalClass: 'col-md-12 staff-credit-mt-10',
+        additionalClass: 'col-12 staff-credit-mt-10rem',
         members: [
         members: [
           { name: 'AND YOU' },
           { name: 'AND YOU' },
         ],
         ],

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

@@ -8,8 +8,8 @@ import {
 
 
 import contributors from './Contributor';
 import contributors from './Contributor';
 
 
-// Unit is px / milli sec
-const scrollSpeed = 0.3;
+// px / sec
+const scrollSpeed = 200;
 
 
 /**
 /**
  * Page staff credit component
  * Page staff credit component
@@ -47,7 +47,7 @@ export default class StaffCredit extends React.Component {
         });
         });
         const target = $('.credit-curtain');
         const target = $('.credit-curtain');
         const scrollTargetHeight = target.children().innerHeight();
         const scrollTargetHeight = target.children().innerHeight();
-        const duration = scrollTargetHeight / scrollSpeed;
+        const duration = scrollTargetHeight / scrollSpeed * 1000;
         target.animate({ scrollTop: scrollTargetHeight }, duration, 'linear');
         target.animate({ scrollTop: scrollTargetHeight }, duration, 'linear');
 
 
         target.slimScroll({
         target.slimScroll({
@@ -105,7 +105,7 @@ export default class StaffCredit extends React.Component {
         return (
         return (
           <React.Fragment key={`${contributor.sectionName}-fragment`}>
           <React.Fragment key={`${contributor.sectionName}-fragment`}>
             <div className={`row ${contributor.additionalClass}`} key={`${contributor.sectionName}-row`}>
             <div className={`row ${contributor.additionalClass}`} key={`${contributor.sectionName}-row`}>
-              <h2 className="col-md-12 dev-team mt-5 staff-credit-mb-10" key={contributor.sectionName}>{contributor.sectionName}</h2>
+              <h2 className="col-md-12 dev-team staff-credit-mt-10rem staff-credit-mb-6rem" key={contributor.sectionName}>{contributor.sectionName}</h2>
               {memberGroups}
               {memberGroups}
             </div>
             </div>
             <div className="clearfix"></div>
             <div className="clearfix"></div>
@@ -113,8 +113,8 @@ export default class StaffCredit extends React.Component {
         );
         );
       });
       });
       return (
       return (
-        <div className="text-center staff-credit-pb-10" onClick={this.deleteCredit}>
-          <h1 className="staff-credit-mb-10">GROWI Contributors</h1>
+        <div className="text-center staff-credit-content" onClick={this.deleteCredit}>
+          <h1 className="staff-credit-mb-6rem">GROWI Contributors</h1>
           <div className="clearfix"></div>
           <div className="clearfix"></div>
           {credit}
           {credit}
         </div>
         </div>

+ 1 - 1
src/client/js/components/User/UserPicture.jsx

@@ -101,7 +101,7 @@ export default class UserPicture extends React.Component {
 
 
 UserPicture.propTypes = {
 UserPicture.propTypes = {
   user: PropTypes.object,
   user: PropTypes.object,
-  size: PropTypes.string,
+  size: PropTypes.oneOf(['xs', 'sm', 'md', 'lg', 'xl']),
   noLink: PropTypes.bool,
   noLink: PropTypes.bool,
   noTooltip: PropTypes.bool,
   noTooltip: PropTypes.bool,
 };
 };

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

@@ -219,6 +219,7 @@ $(() => {
   });
   });
 
 
 
 
+  // TODO GW-2355 remove this after refactoring
   $('#create-page').on('shown.bs.modal', (e) => {
   $('#create-page').on('shown.bs.modal', (e) => {
     // quick hack: replace from server side rendering "date" to client side "date"
     // quick hack: replace from server side rendering "date" to client side "date"
     const today = new Date();
     const today = new Date();

+ 20 - 0
src/client/js/services/AppContainer.js

@@ -34,6 +34,10 @@ export default class AppContainer extends Container {
       preferDarkModeByMediaQuery: false,
       preferDarkModeByMediaQuery: false,
       preferDarkModeByUser: null,
       preferDarkModeByUser: null,
       isDrawerOpened: false,
       isDrawerOpened: false,
+
+      isPageCreateModalShown: false,
+
+      recentlyUpdatedPages: [],
     };
     };
 
 
     const body = document.querySelector('body');
     const body = document.querySelector('body');
@@ -91,6 +95,9 @@ export default class AppContainer extends Container {
       put: this.apiv3Put.bind(this),
       put: this.apiv3Put.bind(this),
       delete: this.apiv3Delete.bind(this),
       delete: this.apiv3Delete.bind(this),
     };
     };
+
+    this.openPageCreateModal = this.openPageCreateModal.bind(this);
+    this.closePageCreateModal = this.closePageCreateModal.bind(this);
   }
   }
 
 
   /**
   /**
@@ -272,6 +279,11 @@ export default class AppContainer extends Container {
     });
     });
   }
   }
 
 
+  async retrieveRecentlyUpdated() {
+    const { data } = await this.apiv3Get('/pages/recent');
+    this.setState({ recentlyUpdatedPages: data.pages });
+  }
+
   fetchUsers() {
   fetchUsers() {
     const interval = 1000 * 60 * 15; // 15min
     const interval = 1000 * 60 * 15; // 15min
     const currentTime = new Date();
     const currentTime = new Date();
@@ -458,4 +470,12 @@ export default class AppContainer extends Container {
     return this.apiv3Request('delete', path, { params });
     return this.apiv3Request('delete', path, { params });
   }
   }
 
 
+  openPageCreateModal() {
+    this.setState({ isPageCreateModalShown: true });
+  }
+
+  closePageCreateModal() {
+    this.setState({ isPageCreateModalShown: false });
+  }
+
 }
 }

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

@@ -7,7 +7,7 @@
     .dropdown-menu {
     .dropdown-menu {
       right: 0;
       right: 0;
       left: auto;
       left: auto;
-      width: 300px;
+      width: 400px;
     }
     }
   }
   }
 
 

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

@@ -56,6 +56,9 @@ body.kibela {
     min-height: 8em;
     min-height: 8em;
     margin: auto;
     margin: auto;
     border-radius: 0.35em;
     border-radius: 0.35em;
+    @include media-breakpoint-down(xs) {
+      top: 0px;
+    }
   }
   }
 
 
   .grw-subnav {
   .grw-subnav {
@@ -73,6 +76,10 @@ body.kibela {
     @media screen and (max-width: 765px) {
     @media screen and (max-width: 765px) {
       padding-top: 30px;
       padding-top: 30px;
     }
     }
+
+    @include media-breakpoint-down(xs) {
+      padding-top: 0px;
+    }
   }
   }
 
 
   .revision-toc {
   .revision-toc {

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

@@ -194,6 +194,10 @@
       color: white;
       color: white;
     }
     }
   }
   }
+
+  .grw-fixed-controls-container {
+    display: none;
+  }
 }
 }
 
 
 .login-page {
 .login-page {

+ 14 - 0
src/client/styles/scss/_page-path.scss

@@ -0,0 +1,14 @@
+.grw-page-path-hierarchical-link {
+  .separator {
+    margin-right: 0.2em;
+    margin-left: 0.2em;
+  }
+}
+
+.grw-page-path-text-muted-container .grw-page-path-hierarchical-link {
+  // overwrite link color
+  &,
+  a {
+    @extend .text-muted;
+  }
+}

+ 0 - 2
src/client/styles/scss/_sidebar.scss

@@ -72,8 +72,6 @@
 }
 }
 
 
 .grw-sidebar-header-container {
 .grw-sidebar-header-container {
-  padding: 10px;
-
   h3 {
   h3 {
     margin-bottom: 0;
     margin-bottom: 0;
   }
   }

+ 10 - 11
src/client/styles/scss/_staff_credit.scss

@@ -2,8 +2,12 @@
 .staff-credit {
 .staff-credit {
   // attached !important for updating from .modal-dialog class style
   // attached !important for updating from .modal-dialog class style
   width: 80vw !important;
   width: 80vw !important;
-  max-width: initial !important;
+  max-width: unset !important;
+
   height: 80vh !important;
   height: 80vh !important;
+  max-height: unset !important;
+
+  margin: 10vh 10vw !important;
 
 
   // see https://css-tricks.com/old-timey-terminal-styling/
   // see https://css-tricks.com/old-timey-terminal-styling/
   @mixin old-timey-terminal-styling() {
   @mixin old-timey-terminal-styling() {
@@ -46,20 +50,15 @@
     font-size: 1.8em;
     font-size: 1.8em;
   }
   }
 
 
-  .staff-credit-mt-10 {
-    margin-top: 6rem;
+  .staff-credit-mt-10rem {
+    margin-top: 10rem;
   }
   }
 
 
-  .staff-credit-mb-10 {
+  .staff-credit-mb-6rem {
     margin-bottom: 6rem;
     margin-bottom: 6rem;
   }
   }
 
 
-  .staff-credit-my-10 {
-    @extend .staff-credit-mt-10;
-    @extend .staff-credit-mb-10;
-  }
-
-  .staff-credit-pb-10 {
-    padding-bottom: 6rem;
+  .staff-credit-content {
+    padding-bottom: 40vh;
   }
   }
 }
 }

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

@@ -47,6 +47,7 @@
 @import 'notification';
 @import 'notification';
 @import 'on-edit';
 @import 'on-edit';
 @import 'page_list';
 @import 'page_list';
+@import 'page-path';
 @import 'page';
 @import 'page';
 @import 'search';
 @import 'search';
 @import 'shortcuts';
 @import 'shortcuts';

+ 0 - 12
src/client/styles/scss/theme/_apply-colors-dark.scss

@@ -183,18 +183,6 @@ ul.pagination {
   }
   }
 }
 }
 
 
-/*
- * GROWI admin page #themeOptions
- */
-.admin-page {
-  #themeOptions {
-    .theme-option-container.active a {
-      background-color: darken(theme-color('primary'), 15%);
-      border-color: darken(theme-color('primary'), 15%);
-    }
-  }
-}
-
 /*
 /*
  * GROWI comment form
  * GROWI comment form
  */
  */

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

@@ -161,6 +161,11 @@ body.kibela {
         color: $color-link-nabvar;
         color: $color-link-nabvar;
       }
       }
     }
     }
+    #personal-dropdown {
+      a.nav-link {
+        color: $color-global;
+      }
+    }
   }
   }
 
 
   /* h */
   /* h */

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

@@ -77,18 +77,6 @@
   }
   }
 }
 }
 
 
-/*
- * GROWI admin page #themeOptions
- */
-.admin-page {
-  #themeOptions {
-    .theme-option-container.active a {
-      background-color: lighten(theme-color('primary'), 20%);
-      border-color: lighten(theme-color('primary'), 20%);
-    }
-  }
-}
-
 /*
 /*
  * GROWI comment form
  * GROWI comment form
  */
  */

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

@@ -47,12 +47,15 @@ $input-focus-color: $color-global;
   .list-group-item {
   .list-group-item {
     color: $color-list;
     color: $color-list;
     background-color: $bgcolor-list;
     background-color: $bgcolor-list;
-    &:hover {
-      background-color: $color-list-hover;
-    }
-    &.active {
-      color: $color-list-active;
-      background-color: $bgcolor-list-active;
+
+    &.list-group-item-action {
+      &:hover {
+        background-color: $color-list-hover;
+      }
+      &.active {
+        color: $color-list-active;
+        background-color: $bgcolor-list-active;
+      }
     }
     }
   }
   }
 }
 }
@@ -118,12 +121,12 @@ $input-focus-color: $color-global;
     }
     }
   }
   }
   div[data-testid='GlobalNavigation'] {
   div[data-testid='GlobalNavigation'] {
-    div {
+    > div {
       background-color: $bgcolor-sidebar;
       background-color: $bgcolor-sidebar;
     }
     }
   }
   }
   div[data-testid='ContextualNavigation'] {
   div[data-testid='ContextualNavigation'] {
-    div {
+    > div {
       color: $color-sidebar-context;
       color: $color-sidebar-context;
       background-color: $bgcolor-sidebar-context;
       background-color: $bgcolor-sidebar-context;
     }
     }
@@ -331,6 +334,10 @@ mark.rbt-highlight-text {
       .theme-option-name {
       .theme-option-name {
         color: $color-global;
         color: $color-global;
       }
       }
+      a {
+        background-color: $color-theme-color-box;
+        border-color: $color-theme-color-box;
+      }
     }
     }
   }
   }
 }
 }

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

@@ -86,6 +86,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($themecolor, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 

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

@@ -74,6 +74,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($themecolor, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 
@@ -207,6 +210,6 @@ html[dark] {
 
 
   .search-top .dropdown-toggle {
   .search-top .dropdown-toggle {
     color: black;
     color: black;
-    background-color: hsla(0,0%,100%,.8);
+    background-color: hsla(0, 0%, 100%, 0.8);
   }
   }
-}
+}

+ 6 - 0
src/client/styles/scss/theme/default.scss

@@ -56,6 +56,9 @@ html[light] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-reversal;
   $color-dropdown-link-hover: $color-reversal;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($primary, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 
@@ -117,6 +120,9 @@ html[dark] {
   $color-dropdown-link-active: $color-global;
   $color-dropdown-link-active: $color-global;
   $color-dropdown-link-hover: $color-reversal;
   $color-dropdown-link-hover: $color-reversal;
 
 
+  // admin theme box
+  $color-theme-color-box: $primary;
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 

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

@@ -50,6 +50,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($primary, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 

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

@@ -73,6 +73,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($primary, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 

+ 181 - 6
src/client/styles/scss/theme/island.scss

@@ -1,8 +1,183 @@
-// import colors
-@import '../../agile-admin/inverse/colors/island';
+@import '../variables';
+@import '../override-bootstrap-variables';
 
 
-// apply agile-admin theme
-@import '../../agile-admin/inverse/style';
+$color-themelight: rgba(183, 226, 219, 1);
+$color-primary: #97cbc3;
+$color-navbar: #0c2a44;
+$color-global: #3c6d72;
+$color-link-global: $color-global;
+$color-inline-code: #8f5313;
+$color-active-bgnav-tabs: #dbf0ed;
+$color-link-wiki: $color-global;
+$color-link-wiki-hover: rgba($color-global, 0.8);
+$bgcolor-inline-code: darken($color-themelight, 3%);
 
 
-// override
-@import 'override-agileadmin';
+$dark: darken($color-global, 5%);
+
+html[light],
+html[dark] {
+  // Background colors
+  $bgcolor-card: #f5f5f5;
+  $bgcolor-global: lighten($color-themelight, 10%);
+  $bgcolor-navbar: $color-navbar;
+
+  // Font colors
+  $color-reversal: #eeeeee;
+  $color-link: lighten($color-global, 20%);
+  $color-link-hover: lighten($color-link, 20%);
+  $color-link-nabvar: $color-reversal;
+
+  // List Group colors
+  $color-list: $color-global;
+  $bgcolor-list: lighten($color-themelight, 10%);
+  $color-list-active: $color-reversal;
+  $bgcolor-list-active: $color-primary;
+  $color-list-hover: $color-reversal;
+
+  // Logo colors
+  $bgcolor-logo: $color-navbar;
+  $fillcolor-logo-mark: lighten(desaturate($bgcolor-inline-code, 10%), 15%);
+
+  // Icon colors
+  $color-editor-icons: $color-global;
+
+  // Border colors
+  $border-color-theme: #ccc;
+
+  // 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($primary, 20%);
+
+  // alert
+  $color-alert: $color-reversal;
+
+  // badge
+  $color-badge: $color-reversal;
+
+  // Sidebar
+  $bgcolor-sidebar: $color-navbar;
+  $color-sidebar-context: $color-reversal;
+  $bgcolor-sidebar-context: lighten($color-navbar, 10%);
+
+  @import 'apply-colors';
+  @import 'apply-colors-light';
+
+  .wiki {
+    .highlighted {
+      background-color: lighten($color-primary, 20%);
+    }
+  }
+
+  .nav-tabs,
+  .nav-tabs .nav-link.active,
+  .nav-link {
+    background: none;
+    border-color: $color-primary;
+    border-bottom-color: $color-primary;
+  }
+
+  .card,
+  .card-header {
+    background: none;
+    border: none;
+  }
+
+  .panel {
+    &,
+    &.panel-white,
+    &.panel-default {
+      color: $color-primary;
+      background-color: lighten($color-primary, 30%);
+      border-color: white;
+
+      .panel-heading {
+        color: $color-primary;
+        background-color: white;
+      }
+
+      ul {
+        li {
+          a {
+            color: darken($color-primary, 15%);
+          }
+        }
+      }
+    }
+  }
+
+  /* GROWI page list */
+  .page-list {
+    .page-list-ul {
+      > li {
+        > a strong {
+          color: $color-link-global;
+        }
+      }
+    }
+  }
+
+  .rbt-menu {
+    background: lighten($color-themelight, 5%);
+  }
+
+  #wrapper > #page-wrapper,
+  .page-editor-preview-container {
+    background-image: url('/images/themes/island/island.png');
+    background-attachment: fixed;
+  }
+
+  /*
+   * Tabs
+   */
+  body:not(.on-edit) .nav.nav-tabs {
+    > li.active > a {
+      background: linear-gradient(
+        rgba($color-active-bgnav-tabs, 0) 0%,
+        rgba($color-active-bgnav-tabs, 0) 90%,
+        $color-active-bgnav-tabs 100%
+      ); // overwrite only the bottom pixel
+    }
+  }
+
+  /* Table */
+  .table > thead > tr > th,
+  .table > tbody > tr > th,
+  .table > tfoot > tr > th,
+  .table > thead > tr > td,
+  .table > tbody > tr > td,
+  .table > tfoot > tr > td,
+  .table > thead > tr > th,
+  .table-bordered {
+    border-top: 1px solid $color-primary;
+  }
+
+  .table-bordered > thead > tr > th,
+  .table-bordered > tbody > tr > th,
+  .table-bordered > tfoot > tr > th,
+  .table-bordered > thead > tr > td,
+  .table-bordered > tbody > tr > td,
+  .table-bordered > tfoot > tr > td {
+    border: 1px solid $color-primary;
+  }
+
+  .table > thead > tr > th {
+    border-bottom: 1px solid $color-primary;
+  }
+
+  .table-bordered {
+    border: 1px solid $color-primary;
+  }
+
+  // login page
+  .nologin {
+    &.login-page {
+      > #wrapper > #page-wrapper {
+        background-image: unset;
+      }
+    }
+  }
+}

+ 6 - 0
src/client/styles/scss/theme/kibela.scss

@@ -53,6 +53,9 @@ html[light] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($bgcolor-theme, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 
@@ -116,6 +119,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($bgcolor-theme, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 

+ 7 - 7
src/client/styles/scss/theme/mono-blue.scss

@@ -1,8 +1,6 @@
 @import '../variables';
 @import '../variables';
 @import '../override-bootstrap-variables';
 @import '../override-bootstrap-variables';
 
 
-
-
 html[light] {
 html[light] {
   // Theme colors
   // Theme colors
   $themecolor: #00587a;
   $themecolor: #00587a;
@@ -29,7 +27,6 @@ html[light] {
   $color-inline-code: $subthemecolor;
   $color-inline-code: $subthemecolor;
   $color-search: #c0d6df;
   $color-search: #c0d6df;
 
 
-
   // List Group colors
   // List Group colors
   $color-list: $color-global;
   $color-list: $color-global;
   $bgcolor-list: transparent;
   $bgcolor-list: transparent;
@@ -51,6 +48,9 @@ html[light] {
   $bgcolor-dropdown-link-active: $primary;
   $bgcolor-dropdown-link-active: $primary;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($primary, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 
@@ -62,7 +62,6 @@ html[light] {
   $color-sidebar-context: $color-reversal;
   $color-sidebar-context: $color-reversal;
   $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
   $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
 
 
-
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-light';
   @import 'apply-colors-light';
 
 
@@ -119,7 +118,6 @@ html[dark] {
   $color-inline-code: $subthemecolor;
   $color-inline-code: $subthemecolor;
   $color-search: #000102;
   $color-search: #000102;
 
 
-
   // List Group colors
   // List Group colors
   $color-list: $color-global;
   $color-list: $color-global;
   $bgcolor-list: transparent;
   $bgcolor-list: transparent;
@@ -142,6 +140,9 @@ html[dark] {
   $bgcolor-dropdown-link-active: $primary;
   $bgcolor-dropdown-link-active: $primary;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
 
 
+  // admin theme box
+  $color-theme-color-box: $primary;
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 
@@ -153,7 +154,6 @@ html[dark] {
   $color-sidebar-context: $color-reversal;
   $color-sidebar-context: $color-reversal;
   $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
   $bgcolor-sidebar-context: lighten($bgcolor-sidebar, 10%);
 
 
-
   @import 'apply-colors';
   @import 'apply-colors';
   @import 'apply-colors-dark';
   @import 'apply-colors-dark';
 
 
@@ -188,4 +188,4 @@ html[dark] {
   .table {
   .table {
     color: white;
     color: white;
   }
   }
-}
+}

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

@@ -93,6 +93,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($primary, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 

+ 26 - 9
src/client/styles/scss/theme/spring.scss

@@ -19,7 +19,7 @@
   .page-editor-preview-container {
   .page-editor-preview-container {
     background-image: url('/images/themes/spring/spring02.svg');
     background-image: url('/images/themes/spring/spring02.svg');
     background-attachment: fixed;
     background-attachment: fixed;
-    background-position: center center;
+    background-position: bottom;
     background-size: cover;
     background-size: cover;
   }
   }
 }
 }
@@ -28,7 +28,7 @@
   #page-wrapper {
   #page-wrapper {
     background-image: url('/images/themes/spring/spring.svg');
     background-image: url('/images/themes/spring/spring.svg');
     background-attachment: fixed;
     background-attachment: fixed;
-    background-position: center center;
+    background-position: bottom;
     background-size: cover;
     background-size: cover;
   }
   }
 }
 }
@@ -57,21 +57,21 @@ html[dark] {
 
 
   // Font colors
   // Font colors
   $color-global: black;
   $color-global: black;
-  $color-reversal: #eeeeee;
+  $color-reversal: white;
   // $color-header: #2b2b2b;
   // $color-header: #2b2b2b;
   $color-link: lighten($color-global, 20%);
   $color-link: lighten($color-global, 20%);
-  $color-link-hover: lighten($color-link, 20%);
+  $color-link-hover: $subthemecolor;
   $color-link-wiki: $subthemecolor;
   $color-link-wiki: $subthemecolor;
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
   $color-link-wiki-hover: lighten($color-link-wiki, 20%);
-  $color-link-nabvar: $color-reversal;
+  $color-link-nabvar: $bgcolor-global;
   $color-inline-code: #c7254e;
   $color-inline-code: #c7254e;
 
 
   // List Group colors
   // List Group colors
   $color-list: $color-global;
   $color-list: $color-global;
-  $bgcolor-list: $bgcolor-global;
-  $color-list-active: $color-reversal;
-  $bgcolor-list-active: $primary;
-  $color-list-hover: $color-reversal;
+  $bgcolor-list: $themelight;
+  $color-list-active: $bgcolor-global;
+  $bgcolor-list-active: $accentcolor;
+  $color-list-hover: lighten($accentcolor, 20%);
 
 
   // Logo colors
   // Logo colors
   $bgcolor-logo: $bgcolor-navbar;
   $bgcolor-logo: $bgcolor-navbar;
@@ -88,6 +88,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: darken($primary, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 
@@ -105,9 +108,23 @@ html[dark] {
   .table {
   .table {
     background-color: $bgcolor-global;
     background-color: $bgcolor-global;
   }
   }
+
   .card-timeline > .card-header {
   .card-timeline > .card-header {
     background-color: $third-main-color;
     background-color: $third-main-color;
   }
   }
+
+  h1,
+  h2 {
+    color: $subthemecolor;
+  }
+
+  .nav.nav-tabs {
+    > .nav-item {
+      > .nav-link.active {
+        color: $subthemecolor;
+      }
+    }
+  }
 }
 }
 
 
 //== Dark Mode
 //== Dark Mode

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

@@ -12,7 +12,6 @@
 // $dark: #;
 // $dark: #;
 
 
 .growi:not(.login-page) {
 .growi:not(.login-page) {
-
   // add background-image
   // add background-image
   #page-wrapper,
   #page-wrapper,
   .page-editor-preview-container {
   .page-editor-preview-container {
@@ -79,6 +78,9 @@ html[dark] {
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-active: $color-reversal;
   $color-dropdown-link-hover: $color-global;
   $color-dropdown-link-hover: $color-global;
 
 
+  // admin theme box
+  $color-theme-color-box: lighten($primary, 20%);
+
   // alert
   // alert
   $color-alert: $color-reversal;
   $color-alert: $color-reversal;
 
 
@@ -103,7 +105,6 @@ html[dark] {
   .grw-navbar {
   .grw-navbar {
     background-image: url('/images/themes/wood/wood-navbar.jpg');
     background-image: url('/images/themes/wood/wood-navbar.jpg');
     border-bottom: $accentcolor 4px solid;
     border-bottom: $accentcolor 4px solid;
-
   }
   }
 }
 }
 
 

+ 15 - 9
src/lib/components/PagePathHierarchicalLink.jsx

@@ -39,32 +39,38 @@ const PagePathHierarchicalLink = (props) => {
   const isParentExists = linkedPagePath.parent != null;
   const isParentExists = linkedPagePath.parent != null;
   const isParentRoot = linkedPagePath.parent?.isRoot;
   const isParentRoot = linkedPagePath.parent?.isRoot;
   const isSeparatorRequired = isParentExists && !isParentRoot;
   const isSeparatorRequired = isParentExists && !isParentRoot;
-  const isParentInTrash = isInTrash || linkedPagePath.isInTrash;
 
 
   const href = encodeURI(urljoin(basePath || '/', linkedPagePath.href));
   const href = encodeURI(urljoin(basePath || '/', linkedPagePath.href));
 
 
+  // eslint-disable-next-line react/prop-types
+  const RootElm = ({ children }) => {
+    return props.isInnerElem
+      ? <>{children}</>
+      : <span className="grw-page-path-hierarchical-link">{children}</span>;
+  };
+
   return (
   return (
-    <>
+    <RootElm>
       { isParentExists && (
       { isParentExists && (
-        <PagePathHierarchicalLink
-          linkedPagePath={linkedPagePath.parent}
-          basePath={basePath}
-          isInTrash={isParentInTrash}
-        />
+        <PagePathHierarchicalLink linkedPagePath={linkedPagePath.parent} basePath={basePath} isInnerElem />
       ) }
       ) }
       { isSeparatorRequired && (
       { isSeparatorRequired && (
         <span className="separator">/</span>
         <span className="separator">/</span>
       ) }
       ) }
 
 
       <a className="page-segment" href={href}>{linkedPagePath.pathName}</a>
       <a className="page-segment" href={href}>{linkedPagePath.pathName}</a>
-    </>
+    </RootElm>
   );
   );
 };
 };
 
 
 PagePathHierarchicalLink.propTypes = {
 PagePathHierarchicalLink.propTypes = {
   linkedPagePath: PropTypes.instanceOf(LinkedPagePath).isRequired,
   linkedPagePath: PropTypes.instanceOf(LinkedPagePath).isRequired,
   basePath: PropTypes.string,
   basePath: PropTypes.string,
-  isInTrash: PropTypes.bool,
+
+  // !!INTERNAL USE ONLY!!
+  isInnerElem: PropTypes.bool,
+
+  isInTrash: PropTypes.bool, // TODO: omit
 };
 };
 
 
 export default PagePathHierarchicalLink;
 export default PagePathHierarchicalLink;

+ 10 - 19
src/server/models/page.js

@@ -155,30 +155,25 @@ class PageQueryBuilder {
     // eslint-disable-next-line no-param-reassign
     // eslint-disable-next-line no-param-reassign
     path = addSlashOfEnd(path);
     path = addSlashOfEnd(path);
 
 
-    // add option to escape the regex strings
-    const combinedOption = Object.assign({ isRegExpEscapedFromPath: true }, option);
-
-    this.addConditionToListByStartWith(path, combinedOption);
-
+    this.addConditionToListByStartWith(path, option);
     return this;
     return this;
   }
   }
 
 
   /**
   /**
    * generate the query to find pages that start with `path`
    * generate the query to find pages that start with `path`
    *
    *
-   * (GROWI) If 'isRegExpEscapedFromPath' is true, `path` should have `/` at the end
-   *   -> returns '{path}/*' and '{path}' self.
-   * (Crowi) If 'isRegExpEscapedFromPath' is false and `path` has `/` at the end
-   *   -> returns '{path}*'
-   * (Crowi) If 'isRegExpEscapedFromPath' is false and `path` doesn't have `/` at the end
-   *   -> returns '{path}*'
+   * In normal case, returns '{path}/*' and '{path}' self.
+   * If top page, return without doing anything.
    *
    *
    * *option*
    * *option*
-   *   - isRegExpEscapedFromPath -- if true, the regex strings included in `path` is escaped (default: false)
+   *   Left for backward compatibility
    */
    */
   addConditionToListByStartWith(path, option) {
   addConditionToListByStartWith(path, option) {
+    // No request is set for the top page
+    if (isTopPage(path)) {
+      return this;
+    }
     const pathCondition = [];
     const pathCondition = [];
-    const isRegExpEscapedFromPath = option.isRegExpEscapedFromPath || false;
 
 
     /*
     /*
      * 1. add condition for finding the page completely match with `path` w/o last slash
      * 1. add condition for finding the page completely match with `path` w/o last slash
@@ -188,13 +183,10 @@ class PageQueryBuilder {
       pathSlashOmitted = path.substr(0, path.length - 1);
       pathSlashOmitted = path.substr(0, path.length - 1);
       pathCondition.push({ path: pathSlashOmitted });
       pathCondition.push({ path: pathSlashOmitted });
     }
     }
-
     /*
     /*
      * 2. add decendants
      * 2. add decendants
      */
      */
-    const pattern = (isRegExpEscapedFromPath)
-      ? escapeStringRegexp(path) // escape
-      : pathSlashOmitted;
+    const pattern = escapeStringRegexp(path); // escape
 
 
     let queryReg;
     let queryReg;
     try {
     try {
@@ -205,7 +197,6 @@ class PageQueryBuilder {
       // force to escape
       // force to escape
       queryReg = new RegExp(`^${escapeStringRegexp(pattern)}`);
       queryReg = new RegExp(`^${escapeStringRegexp(pattern)}`);
     }
     }
-
     pathCondition.push({ path: queryReg });
     pathCondition.push({ path: queryReg });
 
 
     this.query = this.query
     this.query = this.query
@@ -512,7 +503,7 @@ module.exports = function(crowi) {
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     grantLabels[GRANT_RESTRICTED] = 'Anyone with the link'; // リンクを知っている人のみ
     // grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
     // grantLabels[GRANT_SPECIFIED]  = 'Specified users only'; // 特定ユーザーのみ
     grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
     grantLabels[GRANT_USER_GROUP] = 'Only inside the group'; // 特定グループのみ
-    grantLabels[GRANT_OWNER] = 'Just me'; // 自分のみ
+    grantLabels[GRANT_OWNER] = 'Only me'; // 自分のみ
 
 
     return grantLabels;
     return grantLabels;
   };
   };

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

@@ -19,6 +19,46 @@ module.exports = (crowi) => {
 
 
   const Page = crowi.model('Page');
   const Page = crowi.model('Page');
 
 
+  /**
+   * @swagger
+   *
+   *    /pages/recent:
+   *      get:
+   *        tags: [Pages]
+   *        description: Get recently updated pages
+   *        responses:
+   *          200:
+   *            description: Return pages recently updated
+   *
+   */
+  router.get('/recent', loginRequired, async(req, res) => {
+    const limit = 20;
+    const offset = parseInt(req.query.offset) || 0;
+
+    const queryOptions = {
+      offset,
+      limit,
+      includeTrashed: false,
+      isRegExpEscapedFromPath: true,
+      sort: 'updatedAt',
+      desc: -1,
+    };
+
+    try {
+      const result = await Page.findListWithDescendants('/', req.user, queryOptions);
+      if (result.pages.length > limit) {
+        result.pages.pop();
+      }
+
+      return res.apiv3(result);
+    }
+    catch (err) {
+      res.code = 'unknown';
+      logger.error('Failed to get recent pages', err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
   /**
   /**
   * @swagger
   * @swagger
   *
   *

+ 1 - 1
src/server/service/import.js

@@ -253,7 +253,7 @@ class ImportService {
           callback();
           callback();
         },
         },
         final(callback) {
         final(callback) {
-          logger.info(`Importing ${collectionName} has terminated.`);
+          logger.info(`Importing ${collectionName} has completed.`);
           callback();
           callback();
         },
         },
       });
       });

+ 1 - 1
src/server/service/search-delegator/elasticsearch.js

@@ -468,7 +468,7 @@ class ElasticsearchDelegator {
         callback();
         callback();
       },
       },
       final(callback) {
       final(callback) {
-        logger.info(`Adding pages has terminated: (totalCount=${totalCount}, skipped=${skipped})`);
+        logger.info(`Adding pages has completed: (totalCount=${totalCount}, skipped=${skipped})`);
 
 
         if (isEmittingProgressEvent) {
         if (isEmittingProgressEvent) {
           searchEvent.emit('finishAddPage', totalCount, count, skipped);
           searchEvent.emit('finishAddPage', totalCount, count, skipped);

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

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

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

@@ -107,6 +107,7 @@
       {% endif %}
       {% endif %}
 
 
       {% if user %}
       {% if user %}
+      <!-- TODO GW-79 enable after refactoring  <li id="create-page-button" class="nav-item d-none d-md-block"></li> -->
       <li class="nav-item d-none d-md-block">
       <li class="nav-item d-none d-md-block">
         <a class="nav-link create-page px-4" href="#" data-target="#create-page" data-toggle="modal">
         <a class="nav-link create-page px-4" href="#" data-target="#create-page" data-toggle="modal">
           <i class="icon-pencil mr-2"></i>
           <i class="icon-pencil mr-2"></i>
@@ -148,6 +149,7 @@
 
 
 <div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
 <div class="grw-fixed-controls-container d-md-none d-edit-none animated fadeInUp faster">
   <div class="grw-fixed-controls-button-container rounded-circle">
   <div class="grw-fixed-controls-button-container rounded-circle">
+    <!-- TODO GW-79 enable after refactoring <div id='create-page-button-icon'></div> -->
     <button class="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" data-target="#create-page" data-toggle="modal">
     <button class="btn btn-lg btn-primary rounded-circle waves-effect waves-light" type="button" data-target="#create-page" data-toggle="modal">
       <i class="icon-pencil"></i>
       <i class="icon-pencil"></i>
     </button>
     </button>
@@ -157,6 +159,7 @@
 <!-- /#staff-credit -->
 <!-- /#staff-credit -->
 <div id="staff-credit"></div>
 <div id="staff-credit"></div>
 
 
+<!-- TODO GW-79 enable after refactoring <div id="page-create-modal"></div> -->
 {% include '../modal/shortcuts.html' %}
 {% include '../modal/shortcuts.html' %}
 
 
 {% block body_end %}
 {% block body_end %}

+ 2 - 1
src/server/views/modal/create_page.html

@@ -1,3 +1,4 @@
+<!-- TODO GW-2362 remove after adjust layout -->
 <div class="modal create-page" id="create-page">
 <div class="modal create-page" id="create-page">
   <div class="modal-dialog modal-lg">
   <div class="modal-dialog modal-lg">
     <div class="modal-content">
     <div class="modal-content">
@@ -52,7 +53,7 @@
         {% set templateParentPath = parentPath(path | preventXss | escape) %}
         {% set templateParentPath = parentPath(path | preventXss | escape) %}
         <div id="template-form" class="row form-group">
         <div id="template-form" class="row form-group">
           <fieldset class="col-12">
           <fieldset class="col-12">
-            <h3 class="grw-modal-head pb-2">{{ t('template.modal_label.Create template under', templateParentPath) }}</h3>
+            <h3 class="grw-modal-head pb-2">{{ t('template.modal_label.Create template under') }}<br><code>{{ templateParentPath }}</code></h3>
             <div class="d-flex create-page-input-container">
             <div class="d-flex create-page-input-container">
               <div class="create-page-input-row d-flex align-items-center">
               <div class="create-page-input-row d-flex align-items-center">
 
 

+ 1 - 1
src/server/views/modal/delete.html

@@ -56,7 +56,7 @@
                 <input type="hidden" name="completely" value="true">
                 <input type="hidden" name="completely" value="true">
                 <button type="submit" class="m-l-10 btn btn-danger delete-button">
                 <button type="submit" class="m-l-10 btn btn-danger delete-button">
                   <i class="icon-fire" aria-hidden="true"></i>
                   <i class="icon-fire" aria-hidden="true"></i>
-                  {{ t('delete_completely') }}
+                  {{ t('modal_delete.delete_completely') }}
                 </button>
                 </button>
               {% else %}
               {% else %}
                 <button type="submit" class="m-l-10 btn btn-primary delete-button">
                 <button type="submit" class="m-l-10 btn btn-primary delete-button">

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

@@ -18,7 +18,7 @@
   >
   >
 {% else %}
 {% else %}
 <div id="content-main" class="content-main"
 <div id="content-main" class="content-main"
-  data-path="{{ encodeURI(page.path) }}"
+  data-path="{{ encodeURI(path) }}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-current-user="{% if user %}{{ user._id.toString() }}{% endif %}"
   data-slack-channels="{{ slack|default('') }}"
   data-slack-channels="{{ slack|default('') }}"
   >
   >

+ 2 - 13
src/test/models/page.test.js

@@ -267,14 +267,14 @@ describe('Page', () => {
         expect(page.path).toEqual(expectedPage.path);
         expect(page.path).toEqual(expectedPage.path);
       });
       });
 
 
-      test('should find page (just me)', async() => {
+      test('should find page (only me)', async() => {
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser0);
         expect(page).not.toBeNull();
         expect(page).not.toBeNull();
         expect(page.path).toEqual(expectedPage.path);
         expect(page.path).toEqual(expectedPage.path);
       });
       });
 
 
-      test('should not be found by grant (just me)', async() => {
+      test('should not be found by grant (only me)', async() => {
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const expectedPage = await Page.findOne({ path: '/grant/owner' });
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser1);
         const page = await Page.findByIdAndViewer(expectedPage.id, testUser1);
         expect(page).toBeNull();
         expect(page).toBeNull();
@@ -334,16 +334,5 @@ describe('Page', () => {
       expect(pagePaths).toContainEqual('/page2');
       expect(pagePaths).toContainEqual('/page2');
     });
     });
 
 
-    test('should process with regexp', async() => {
-      const result = await Page.findListByStartWith('/page\\d{1}/', testUser0, {});
-
-      // assert totalCount
-      expect(result.totalCount).toEqual(3);
-      // assert paths
-      const pagePaths = result.pages.map((page) => { return page.path });
-      expect(pagePaths).toContainEqual('/page1');
-      expect(pagePaths).toContainEqual('/page1/child1');
-      expect(pagePaths).toContainEqual('/page2');
-    });
   });
   });
 });
 });