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

Merge branch 'feat/search-implement' into feat/77525-enable-to-delete-pages

# Conflicts:
#	packages/app/resource/locales/en_US/translation.json
#	packages/app/resource/locales/ja_JP/translation.json
#	packages/app/resource/locales/zh_CN/translation.json
#	packages/app/src/components/SearchPage.jsx
#	packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx
#	packages/app/src/components/SearchPage/SearchControl.tsx
#	packages/app/src/components/SearchPage/SearchResultList.tsx
#	packages/app/src/components/SearchPage/SearchResultListItem.tsx
Yohei-Shiina 4 лет назад
Родитель
Сommit
becd8cefb6
58 измененных файлов с 632 добавлено и 464 удалено
  1. 31 1
      CHANGELOG.md
  2. 1 1
      lerna.json
  3. 1 1
      package.json
  4. 2 2
      packages/app/docker/README.md
  5. 8 8
      packages/app/package.json
  6. 8 4
      packages/app/resource/locales/en_US/translation.json
  7. 8 4
      packages/app/resource/locales/ja_JP/translation.json
  8. 8 5
      packages/app/resource/locales/zh_CN/translation.json
  9. 0 49
      packages/app/src/client/services/PageContainer.js
  10. 47 17
      packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx
  11. 24 12
      packages/app/src/components/BookmarkButton.jsx
  12. 15 1
      packages/app/src/components/Navbar/SubNavButtons.jsx
  13. 9 1
      packages/app/src/components/Page/PageManagement.jsx
  14. 4 1
      packages/app/src/components/Page/TrashPageAlert.jsx
  15. 17 11
      packages/app/src/components/PageDeleteModal.jsx
  16. 7 5
      packages/app/src/components/PageDuplicateModal.jsx
  17. 1 1
      packages/app/src/components/PageList/Page.jsx
  18. 25 22
      packages/app/src/components/PageRenameModal.jsx
  19. 13 12
      packages/app/src/components/PutbackPageModal.jsx
  20. 33 29
      packages/app/src/components/SearchPage.jsx
  21. 2 1
      packages/app/src/components/SearchPage/DeletePageListModal.jsx
  22. 4 3
      packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx
  23. 37 27
      packages/app/src/components/SearchPage/SearchControl.tsx
  24. 17 18
      packages/app/src/components/SearchPage/SearchPageForm.jsx
  25. 28 7
      packages/app/src/components/SearchPage/SearchPageLayout.tsx
  26. 28 31
      packages/app/src/components/SearchPage/SearchResultContent.tsx
  27. 12 18
      packages/app/src/components/SearchPage/SearchResultList.tsx
  28. 18 26
      packages/app/src/components/SearchPage/SearchResultListItem.tsx
  29. 1 1
      packages/app/src/components/SearchTypeahead.jsx
  30. 1 3
      packages/app/src/components/Sidebar/RecentChanges.jsx
  31. 1 1
      packages/app/src/components/User/SeenUserInfo.jsx
  32. 13 0
      packages/app/src/interfaces/search.ts
  33. 1 0
      packages/app/src/server/models/config.ts
  34. 22 12
      packages/app/src/server/routes/search.js
  35. 0 6
      packages/app/src/server/service/config-manager.ts
  36. 16 3
      packages/app/src/server/service/passport.ts
  37. 1 2
      packages/app/src/server/service/search.js
  38. 23 38
      packages/app/src/server/service/slack-command-handler/note.js
  39. 1 1
      packages/app/src/server/views/search.html
  40. 39 15
      packages/app/src/styles/_search.scss
  41. 8 1
      packages/app/src/styles/theme/_apply-colors.scss
  42. 4 1
      packages/app/tsconfig.base.json
  43. 1 1
      packages/codemirror-textlint/package.json
  44. 1 1
      packages/core/package.json
  45. 2 2
      packages/core/src/models/devided-page-path.js
  46. 1 1
      packages/plugin-attachment-refs/package.json
  47. 1 1
      packages/plugin-lsx/package.json
  48. 1 1
      packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx
  49. 1 1
      packages/plugin-pukiwiki-like-linker/package.json
  50. 1 1
      packages/slack/package.json
  51. 2 2
      packages/slackbot-proxy/package.json
  52. 1 1
      packages/ui/package.json
  53. 19 3
      packages/ui/src/components/PagePath/PageListMeta.jsx
  54. 0 40
      packages/ui/src/components/PagePath/PagePathLabel.jsx
  55. 56 0
      packages/ui/src/components/PagePath/PagePathLabel.tsx
  56. 1 3
      packages/ui/src/components/SearchPage/FootstampIcon.jsx
  57. 1 0
      packages/ui/src/index.ts
  58. 4 4
      yarn.lock

+ 31 - 1
CHANGELOG.md

@@ -1,9 +1,39 @@
 # Changelog
 # Changelog
 
 
-## [Unreleased](https://github.com/weseek/growi/compare/v4.4.10...HEAD)
+## [Unreleased](https://github.com/weseek/growi/compare/v4.4.12...HEAD)
 
 
 *Please do not manually update this file. We've automated the process.*
 *Please do not manually update this file. We've automated the process.*
 
 
+## [v4.4.12](https://github.com/weseek/growi/compare/v4.4.11...v4.4.12) - 2021-11-15
+
+### 🐛 Bug Fixes
+
+- fix: Cannot use HackMD (#4667)
+
+### 🧰 Maintenance
+
+- ci(deps): Downgrade passport to 0.4.0 (#4669) @mudana-grune
+
+## [v4.4.11](https://github.com/weseek/growi/compare/v4.4.10...v4.4.11) - 2021-11-12
+
+### 🚀 Improvement
+
+- imprv: SAML settings by DB (#4656) @yuki-takei
+
+### 🐛 Bug Fixes
+
+- fix: Unescape Attribute-based Login Control field value (#4651) @haruhikonyan
+- fix: Slack Integration 'note' command causes expired_trigger_id error (#4629) @stevenfukase
+- fix: Timeline was broken (#4639) @yuki-takei
+
+### 🧰 Maintenance
+
+- support: Bump mpath with mongoose (#4638) @yuki-takei
+- ci(deps): bump passport-oauth2 from 1.4.0 to 1.6.1 (#4599) @dependabot
+- ci(deps): bump passport from 0.4.0 to 0.5.0 (#4582) @dependabot
+- ci(deps): bump axios from 0.21.1 to 0.24.0 (#4604) @dependabot
+- ci(deps): bump tar from 4.4.13 to 4.4.19 (#4601) @dependabot
+
 ## [v4.4.10](https://github.com/weseek/growi/compare/v4.4.9...v4.4.10) - 2021-11-08
 ## [v4.4.10](https://github.com/weseek/growi/compare/v4.4.9...v4.4.10) - 2021-11-08
 
 
 ### 🚀 Improvement
 ### 🚀 Improvement

+ 1 - 1
lerna.json

@@ -1,7 +1,7 @@
 {
 {
   "npmClient": "yarn",
   "npmClient": "yarn",
   "useWorkspaces": true,
   "useWorkspaces": true,
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "packages": [
   "packages": [
     "packages/*"
     "packages/*"
   ]
   ]

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",

+ 2 - 2
packages/app/docker/README.md

@@ -10,8 +10,8 @@ GROWI Official docker image
 Supported tags and respective Dockerfile links
 Supported tags and respective Dockerfile links
 ------------------------------------------------
 ------------------------------------------------
 
 
-* [`4.4.10`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.10/docker/Dockerfile)
-* [`4.4.10-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.10/docker/Dockerfile)
+* [`4.4.12`, `4.4`, `4`, `latest` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.12/docker/Dockerfile)
+* [`4.4.12-nocdn`, `4.4-nocdn`, `4-nocdn`, `latest-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.4.12/docker/Dockerfile)
 * [`4.3.3`, `4.3` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
 * [`4.3.3`, `4.3` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
 * [`4.3.3-nocdn`, `4.3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
 * [`4.3.3-nocdn`, `4.3-nocdn` (Dockerfile)](https://github.com/weseek/growi/blob/v4.3.3/docker/Dockerfile)
 
 

+ 8 - 8
packages/app/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/app",
   "name": "@growi/app",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "//// for production": "",
     "//// for production": "",
@@ -58,11 +58,11 @@
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@browser-bunyan/console-formatted-stream": "^1.6.2",
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
     "@google-cloud/storage": "^5.8.5",
     "@google-cloud/storage": "^5.8.5",
-    "@growi/codemirror-textlint": "^4.4.11-RC.0",
-    "@growi/plugin-attachment-refs": "^4.4.11-RC.0",
-    "@growi/plugin-lsx": "^4.4.11-RC.0",
-    "@growi/plugin-pukiwiki-like-linker": "^4.4.11-RC.0",
-    "@growi/slack": "^4.4.11-RC.0",
+    "@growi/codemirror-textlint": "^4.4.13-RC.0",
+    "@growi/plugin-attachment-refs": "^4.4.13-RC.0",
+    "@growi/plugin-lsx": "^4.4.13-RC.0",
+    "@growi/plugin-pukiwiki-like-linker": "^4.4.13-RC.0",
+    "@growi/slack": "^4.4.13-RC.0",
     "@promster/express": "^5.1.0",
     "@promster/express": "^5.1.0",
     "@promster/server": "^6.0.3",
     "@promster/server": "^6.0.3",
     "@slack/events-api": "^3.0.0",
     "@slack/events-api": "^3.0.0",
@@ -122,7 +122,7 @@
     "nodemailer": "^6.6.2",
     "nodemailer": "^6.6.2",
     "nodemailer-ses-transport": "~1.5.0",
     "nodemailer-ses-transport": "~1.5.0",
     "openid-client": "=2.5.0",
     "openid-client": "=2.5.0",
-    "passport": "^0.5.0",
+    "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-google-oauth20": "^2.0.0",
     "passport-http": "^0.3.0",
     "passport-http": "^0.3.0",
@@ -159,7 +159,7 @@
     "@alienfast/i18next-loader": "^1.0.16",
     "@alienfast/i18next-loader": "^1.0.16",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/drawer": "^5.3.7",
     "@atlaskit/navigation-next": "^8.0.5",
     "@atlaskit/navigation-next": "^8.0.5",
-    "@growi/ui": "^4.4.11-RC.0",
+    "@growi/ui": "^4.4.13-RC.0",
     "@handsontable/react": "=2.1.0",
     "@handsontable/react": "=2.1.0",
     "@types/compression": "^1.7.0",
     "@types/compression": "^1.7.0",
     "@types/express": "^4.17.11",
     "@types/express": "^4.17.11",

+ 8 - 4
packages/app/resource/locales/en_US/translation.json

@@ -568,7 +568,7 @@
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
     "popover_desc": "Input channel name. You can notify multiple channels by entering a comma-separated list."
   },
   },
   "search_result": {
   "search_result": {
-    "result_meta": "Found \"{{keyword}}\" in {{total}}.",
+    "result_meta": "Search results for:",
     "deletion_mode_btn_lavel": "Select and delete page",
     "deletion_mode_btn_lavel": "Select and delete page",
     "cancel": "Cancel",
     "cancel": "Cancel",
     "delete": "Delete",
     "delete": "Delete",
@@ -577,7 +577,10 @@
     "delete_completely": "Delete completely",
     "delete_completely": "Delete completely",
     "include_certain_path" : "Include {{pathToInclude}} path ",
     "include_certain_path" : "Include {{pathToInclude}} path ",
     "delete_all_selected_page" : "Delete All",
     "delete_all_selected_page" : "Delete All",
-    "currently_not_implemented":"This is not currently implemented"
+    "currently_not_implemented":"This is not currently implemented",
+    "number_of_list_to_display" : "Display",
+    "page_number_unit" : "pages"
+
   },
   },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "Guest users access",
     "Guest Users Access": "Guest users access",
@@ -700,8 +703,9 @@
       "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
       "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
       "note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<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 setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
       "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
       "attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
-      "attr_based_login_control_rule_detail": "See <a href=\"https://lucene.apache.org/core/2_9_4/queryparsersyntax.html\" target=\"_blank\">Apache Lucene - Query Parser Syntax</a>.<h6>Supported Queries:</h6><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h6>Unsupported Queries:</h6><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul>",
-      "attr_based_login_control_rule_example": "<h6>Example</h6>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
+      "attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
+      "attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
+      "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
     },
     },
     "Basic": {
     "Basic": {

+ 8 - 4
packages/app/resource/locales/ja_JP/translation.json

@@ -568,7 +568,7 @@
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
     "popover_desc": "チャンネル名を入れてください。カンマ区切りのリストを入力することで複数のチャンネルに通知することができます。"
   },
   },
   "search_result": {
   "search_result": {
-    "result_meta": "{{total}}件のページが見つかりました。検索ワード: \"{{keyword}}\"",
+    "result_meta": "検索結果:",
     "deletion_mode_btn_lavel": "ページを指定して削除",
     "deletion_mode_btn_lavel": "ページを指定して削除",
     "cancel": "キャンセル",
     "cancel": "キャンセル",
     "delete": "削除",
     "delete": "削除",
@@ -577,7 +577,9 @@
     "delete_completely": "完全に削除する",
     "delete_completely": "完全に削除する",
     "include_certain_path": "{{pathToInclude}}下を含む ",
     "include_certain_path": "{{pathToInclude}}下を含む ",
     "delete_all_selected_page" : "一括削除",
     "delete_all_selected_page" : "一括削除",
-    "currently_not_implemented":"現在未実装の機能です"
+    "currently_not_implemented":"現在未実装の機能です",
+    "number_of_list_to_display" : "表示件数",
+    "page_number_unit" : "件"
   },
   },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Guest Users Access": "ゲストユーザーのアクセス",
@@ -697,8 +699,10 @@
       "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します",
       "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します",
       "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
       "note for the only env option": "現在SAML認証のON/OFFの設定値及びハイライトされている設定値は環境変数の値のみを使用するようになっています<br>この設定を変更する場合は環境変数 <code>{{env}}</code> の値をfalseに変更もしくは削除してください",
       "attr_based_login_control_detail": "SAMLの <code>&lt;saml:AttributeStatement&gt;</code> 要素に含まれる <code>&lt;saml:Attribute&gt;</code> 要素と、その子要素 <code>&lt;saml:AttributeValue&gt;</code> を利用してログインの可否を制御します。",
       "attr_based_login_control_detail": "SAMLの <code>&lt;saml:AttributeStatement&gt;</code> 要素に含まれる <code>&lt;saml:Attribute&gt;</code> 要素と、その子要素 <code>&lt;saml:AttributeValue&gt;</code> を利用してログインの可否を制御します。",
-      "attr_based_login_control_rule_detail": "See <a href=\"https://lucene.apache.org/core/2_9_4/queryparsersyntax.html\" target=\"_blank\">Apache Lucene - Query Parser Syntax</a>.<h6>利用可能なクエリ:</h6><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h6>利用不可なクエリ:</h6><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul>",
-      "attr_based_login_control_rule_example": "<h6>Example</h6>ルールに <code>(Department: A || Department: B) && Position: Leader</code> を指定した場合, <code>Department: A</code> または <code>Department: B</code> のどちらかに該当し、かつ <code>Position: Leader</code> を持つユーザーにログインを<strong>許可</strong>します。"
+      "attr_based_login_control_rule_help": "<h5>利用可能なクエリ:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>利用不可なクエリ:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>特殊文字のエスケープ</h5>次の特殊文字はエスケープする必要があります。<code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
+      "attr_based_login_control_rule_example1": "<h5>条件式の例</h5>ルールに <code>(Department: A || Department: B) && Position: Leader</code> を指定した場合, <code>Department: A</code> または <code>Department: B</code> のどちらかに該当し、かつ <code>Position: Leader</code> を持つユーザーにログインを<strong>許可</strong>します。",
+      "attr_based_login_control_rule_exampl2": "<h5>エスケープの例</h5>ルールに URL を利用したい場合は、次のようにエスケープしてください:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
+      "updated_saml": "Succeeded to update SAML setting"
     },
     },
     "Basic": {
     "Basic": {
       "enable_basic": "Basic を有効にする",
       "enable_basic": "Basic を有効にする",

+ 8 - 5
packages/app/resource/locales/zh_CN/translation.json

@@ -683,9 +683,10 @@
 			"Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
 			"Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>{{env}}</code> is used.",
 			"note for the only env option": "The setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<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 setting item that enables or disables the SAML authentication and the highlighted setting items use only the value of environment variables.<br>To change this setting, please change to false or delete the value of the environment variable <code>{{env}}</code> .",
 			"attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
 			"attr_based_login_control_detail": "Limit who can sign up by using <code>&lt;saml: Attribute&gt;</code> element included in <code>&lt;saml: AttributeStatement&gt;</code> element and its child element <code>&lt;saml: AttributeValue&gt;</code>.",
-			"attr_based_login_control_rule_detail": "See <a href=\"https://lucene.apache.org/core/2_9_4/queryparsersyntax.html\" target=\"_blank\">Apache Lucene - Query Parser Syntax</a>.<h6>Supported Queries:</h6><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h6>Unsupported Queries:</h6><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul>",
-			"attr_based_login_control_rule_example": "<h6>Example</h6>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
-			"updated_saml": "Succeeded to update SAML setting"
+			"attr_based_login_control_rule_help": "<h5>Supported Queries:</h5><ul><li>Terms</li><li>Fields</li><li>AND/NOT/OR Operator</li><li>Grouping</li></ul><h5>Unsupported Queries:</h5><ul><li>Wildcard, Fuzzy, Proximity, Range and Boosting</li><li>+/- Operator</li><li>Field Grouping</li></ul><h5>Escaping special characters</h5>It is needed to escape following special characters:<br><code>+ - && || ! ( ) { } [ ] ^ &quot; &tilde; * ? : &#92;</code> and <code>/</code>",
+			"attr_based_login_control_rule_example1": "<h5>Example for conditions</h5>If a rule is <code>(Department: A || Department: B) && Position: Leader</code>, users who have either <code>Department: A</code> or <code>Department: B</code> and have <code>Position: Leader</code> <strong>can</strong> sign in.",
+      "attr_based_login_control_rule_example2": "<h5>Example for escaping</h5>If you would like to use URL as a query value, escape the following:<br><code>http&#92;:&#92;/&#92;/schemas.example.com&#92;/ws&#92;/2005&#92;/05&#92;/identity&#92;/claims&#92;/emailaddress: &quot;myname@example.com&quot;</code>",
+      "updated_saml": "Succeeded to update SAML setting"
 		},
 		},
 		"Basic": {
 		"Basic": {
 			"enable_basic": "Enable Basic",
 			"enable_basic": "Enable Basic",
@@ -840,7 +841,7 @@
 		"use_os_settings": "使用操作系统设置"
 		"use_os_settings": "使用操作系统设置"
 	},
 	},
 	"search_result": {
 	"search_result": {
-		"result_meta": "在{{total}中找到了{{keyword}。",
+		"result_meta": "搜索结果:",
 		"deletion_mode_btn_lavel": "选择并删除页面",
 		"deletion_mode_btn_lavel": "选择并删除页面",
 		"cancel": "取消",
 		"cancel": "取消",
 		"delete": "删除",
 		"delete": "删除",
@@ -849,7 +850,9 @@
 		"delete_completely": "完全删除",
 		"delete_completely": "完全删除",
     "include_certain_path": "包含 {{pathToInclude}} 路径 ",
     "include_certain_path": "包含 {{pathToInclude}} 路径 ",
     "delete_all_selected_page": "删除所有",
     "delete_all_selected_page": "删除所有",
-    "currently_not_implemented": "这是当前未实现的功能"
+    "currently_not_implemented": "这是当前未实现的功能",
+    "number_of_list_to_display" : "显示器的数量",
+    "page_number_unit" : "例"
 	},
 	},
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"to_cloud_settings": "進入 GROWI.cloud 的管理界面",
 	"login": {
 	"login": {

+ 0 - 49
packages/app/src/client/services/PageContainer.js

@@ -328,12 +328,6 @@ export default class PageContainer extends Container {
     });
     });
   }
   }
 
 
-  async toggleBookmark() {
-    const bool = !this.state.isBookmarked;
-    await this.appContainer.apiv3Put('/bookmarks', { pageId: this.state.pageId, bool });
-    return this.retrieveBookmarkInfo();
-  }
-
   async checkAndUpdateImageUrlCached(users) {
   async checkAndUpdateImageUrlCached(users) {
     const noImageCacheUsers = users.filter((user) => { return user.imageUrlCached == null });
     const noImageCacheUsers = users.filter((user) => { return user.imageUrlCached == null });
     if (noImageCacheUsers.length === 0) {
     if (noImageCacheUsers.length === 0) {
@@ -529,49 +523,6 @@ export default class PageContainer extends Container {
     return res;
     return res;
   }
   }
 
 
-  deletePage(isRecursively, isCompletely) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-
-    // control flag
-    const completely = isCompletely ? true : null;
-    const recursively = isRecursively ? true : null;
-
-    return this.appContainer.apiPost('/pages.remove', {
-      recursively,
-      completely,
-      page_id: this.state.pageId,
-      revision_id: this.state.revisionId,
-    });
-
-  }
-
-  revertRemove(isRecursively) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-
-    // control flag
-    const recursively = isRecursively ? true : null;
-
-    return this.appContainer.apiPost('/pages.revertRemove', {
-      recursively,
-      page_id: this.state.pageId,
-    });
-  }
-
-  rename(newPagePath, isRecursively, isRenameRedirect, isRemainMetadata) {
-    const socketIoContainer = this.appContainer.getContainer('SocketIoContainer');
-    const { pageId, revisionId, path } = this.state;
-
-    return this.appContainer.apiv3Put('/pages/rename', {
-      revisionId,
-      pageId,
-      isRecursively,
-      isRenameRedirect,
-      isRemainMetadata,
-      newPagePath,
-      path,
-    });
-  }
-
   showSuccessToastr() {
   showSuccessToastr() {
     toastr.success(undefined, 'Saved successfully', {
     toastr.success(undefined, 'Saved successfully', {
       closeButton: true,
       closeButton: true,

+ 47 - 17
packages/app/src/components/Admin/Security/SamlSecuritySettingContents.jsx

@@ -3,6 +3,8 @@ import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
+import { Collapse } from 'reactstrap';
+
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { withUnstatedContainers } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 import { toastSuccess, toastError } from '~/client/util/apiNotification';
 
 
@@ -15,6 +17,10 @@ class SamlSecurityManagementContents extends React.Component {
   constructor(props) {
   constructor(props) {
     super(props);
     super(props);
 
 
+    this.state = {
+      isHelpOpened: false,
+    };
+
     this.onClickSubmit = this.onClickSubmit.bind(this);
     this.onClickSubmit = this.onClickSubmit.bind(this);
   }
   }
 
 
@@ -112,7 +118,7 @@ class SamlSecurityManagementContents extends React.Component {
               Basic Settings
               Basic Settings
             </h3>
             </h3>
 
 
-            <table className={`table settings-table ${adminSamlSecurityContainer.state.useOnlyEnvVars && 'use-only-env-vars'}`}>
+            <table className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}>
               <colgroup>
               <colgroup>
                 <col className="item-name" />
                 <col className="item-name" />
                 <col className="from-db" />
                 <col className="from-db" />
@@ -222,7 +228,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               Attribute Mapping
               Attribute Mapping
             </h3>
             </h3>
 
 
-            <table className={`table settings-table ${adminSamlSecurityContainer.state.useOnlyEnvVars && 'use-only-env-vars'}`}>
+            <table className="table settings-table">
               <colgroup>
               <colgroup>
                 <col className="item-name" />
                 <col className="item-name" />
                 <col className="from-db" />
                 <col className="from-db" />
@@ -238,7 +244,6 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      readOnly={useOnlyEnvVars}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapId}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapId}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapId(e.target.value)}
                     />
                     />
@@ -266,7 +271,6 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      readOnly={useOnlyEnvVars}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapUsername}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapUsername}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapUserName(e.target.value)}
                     />
                     />
@@ -292,7 +296,6 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      readOnly={useOnlyEnvVars}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapMail}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapMail}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapMail(e.target.value)}
                     />
                     />
@@ -318,7 +321,6 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      readOnly={useOnlyEnvVars}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapFirstName}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapFirstName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapFirstName(e.target.value)}
                     />
                     />
@@ -349,7 +351,6 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     <input
                     <input
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
-                      readOnly={useOnlyEnvVars}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapLastName}
                       defaultValue={adminSamlSecurityContainer.state.samlAttrMapLastName}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
                       onChange={e => adminSamlSecurityContainer.changeSamlAttrMapLastName(e.target.value)}
                     />
                     />
@@ -433,7 +434,7 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
               <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
               <small dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_detail') }} />
             </p>
             </p>
 
 
-            <table className={`table settings-table ${useOnlyEnvVars && 'use-only-env-vars'}`}>
+            <table className="table settings-table">
               <colgroup>
               <colgroup>
                 <col className="item-name" />
                 <col className="item-name" />
                 <col className="from-db" />
                 <col className="from-db" />
@@ -448,22 +449,51 @@ pWVdnzS1VCO8fKsJ7YYIr+JmHvseph3kFUOI5RqkCcMZlKUv83aUThsTHw==
                     { t('security_setting.form_item_name.ABLCRule') }
                     { t('security_setting.form_item_name.ABLCRule') }
                   </th>
                   </th>
                   <td>
                   <td>
-                    <input
+                    <textarea
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
                       defaultValue={adminSamlSecurityContainer.state.samlABLCRule || ''}
                       defaultValue={adminSamlSecurityContainer.state.samlABLCRule || ''}
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
                       onChange={(e) => { adminSamlSecurityContainer.changeSamlABLCRule(e.target.value) }}
-                      readOnly={useOnlyEnvVars}
                     />
                     />
-                    <p className="form-text text-muted">
-                      <small>
-                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_detail') }} />
-                        <span dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example') }} />
-                      </small>
-                    </p>
+                    <div className="mt-2">
+                      <p>
+                        See&nbsp;
+                        <a
+                          href="https://lucene.apache.org/core/2_9_4/queryparsersyntax.html"
+                          target="_blank"
+                          rel="noreferer noreferrer"
+                        >
+                          Apache Lucene - Query Parser Syntax <i className="icon-share-alt"></i>
+                        </a>.
+                      </p>
+                      <div className="accordion" id="accordionExample">
+                        <div className="card">
+                          <div className="card-header p-1">
+                            <h2 className="mb-0">
+                              <button
+                                className="btn btn-link btn-block text-left"
+                                type="button"
+                                onClick={() => this.setState({ isHelpOpened: !this.state.isHelpOpened })}
+                                aria-expanded="true"
+                                aria-controls="ablchelp"
+                              >
+                                <i className={`icon-fw ${this.state.isHelpOpened ? 'icon-arrow-down' : 'icon-arrow-right'} small`}></i> Show more...
+                              </button>
+                            </h2>
+                          </div>
+                          <Collapse isOpen={this.state.isHelpOpened}>
+                            <div className="card-body">
+                              <p dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_help') }} />
+                              <p dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example1') }} />
+                              <p dangerouslySetInnerHTML={{ __html: t('security_setting.SAML.attr_based_login_control_rule_example2') }} />
+                            </div>
+                          </Collapse>
+                        </div>
+                      </div>
+                    </div>
                   </td>
                   </td>
                   <td>
                   <td>
-                    <input
+                    <textarea
                       className="form-control"
                       className="form-control"
                       type="text"
                       type="text"
                       value={adminSamlSecurityContainer.state.envABLCRule || ''}
                       value={adminSamlSecurityContainer.state.envABLCRule || ''}

+ 24 - 12
packages/app/src/components/BookmarkButton.jsx

@@ -6,7 +6,8 @@ import { withTranslation } from 'react-i18next';
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 
 
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
-import PageContainer from '~/client/services/PageContainer';
+import { apiv3Put } from '~/client/util/apiv3-client';
+
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
 
 
 class BookmarkButton extends React.Component {
 class BookmarkButton extends React.Component {
@@ -18,7 +19,9 @@ class BookmarkButton extends React.Component {
   }
   }
 
 
   async handleClick() {
   async handleClick() {
-    const { appContainer, pageContainer } = this.props;
+    const {
+      appContainer, pageId, isBookmarked, onChangeInvoked,
+    } = this.props;
     const { isGuestUser } = appContainer;
     const { isGuestUser } = appContainer;
 
 
     if (isGuestUser) {
     if (isGuestUser) {
@@ -26,16 +29,21 @@ class BookmarkButton extends React.Component {
     }
     }
 
 
     try {
     try {
-      pageContainer.toggleBookmark();
+      const bool = !isBookmarked;
+      await apiv3Put('/bookmarks', { pageId, bool });
+      if (onChangeInvoked != null) {
+        onChangeInvoked();
+      }
     }
     }
     catch (err) {
     catch (err) {
       toastError(err);
       toastError(err);
     }
     }
   }
   }
 
 
-
   render() {
   render() {
-    const { appContainer, pageContainer, t } = this.props;
+    const {
+      appContainer, t, isBookmarked, sumOfBookmarks,
+    } = this.props;
     const { isGuestUser } = appContainer;
     const { isGuestUser } = appContainer;
 
 
     return (
     return (
@@ -45,12 +53,14 @@ class BookmarkButton extends React.Component {
           id="bookmark-button"
           id="bookmark-button"
           onClick={this.handleClick}
           onClick={this.handleClick}
           className={`btn btn-bookmark border-0
           className={`btn btn-bookmark border-0
-          ${`btn-${this.props.size}`} ${pageContainer.state.isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
+          ${`btn-${this.props.size}`} ${isBookmarked ? 'active' : ''} ${isGuestUser ? 'disabled' : ''}`}
         >
         >
           <i className="icon-star mr-3"></i>
           <i className="icon-star mr-3"></i>
-          <span className="total-bookmarks">
-            {pageContainer.state.sumOfBookmarks}
-          </span>
+          {sumOfBookmarks && (
+            <span className="total-bookmarks">
+              {sumOfBookmarks}
+            </span>
+          )}
         </button>
         </button>
 
 
         {isGuestUser && (
         {isGuestUser && (
@@ -67,13 +77,15 @@ class BookmarkButton extends React.Component {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer, PageContainer]);
+const BookmarkButtonWrapper = withUnstatedContainers(BookmarkButton, [AppContainer]);
 
 
 BookmarkButton.propTypes = {
 BookmarkButton.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
-  pageId: PropTypes.string,
+  pageId: PropTypes.string.isRequired,
+  isBookmarked: PropTypes.bool.isRequired,
+  sumOfBookmarks: PropTypes.number,
+  onChangeInvoked: PropTypes.func,
   t: PropTypes.func.isRequired,
   t: PropTypes.func.isRequired,
   size: PropTypes.string,
   size: PropTypes.string,
 };
 };

+ 15 - 1
packages/app/src/components/Navbar/SubNavButtons.jsx

@@ -4,11 +4,14 @@ import AppContainer from '~/client/services/AppContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import NavigationContainer from '~/client/services/NavigationContainer';
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
+import loggerFactory from '~/utils/logger';
 
 
 import BookmarkButton from '../BookmarkButton';
 import BookmarkButton from '../BookmarkButton';
 import LikeButtons from '../LikeButtons';
 import LikeButtons from '../LikeButtons';
 import PageManagement from '../Page/PageManagement';
 import PageManagement from '../Page/PageManagement';
 
 
+const logger = loggerFactory('growi:SubnavButtons');
+
 const SubnavButtons = (props) => {
 const SubnavButtons = (props) => {
   const {
   const {
     appContainer, navigationContainer, pageContainer, isCompactMode,
     appContainer, navigationContainer, pageContainer, isCompactMode,
@@ -18,6 +21,12 @@ const SubnavButtons = (props) => {
 
 
   /* eslint-disable react/prop-types */
   /* eslint-disable react/prop-types */
   const PageReactionButtons = ({ pageContainer }) => {
   const PageReactionButtons = ({ pageContainer }) => {
+    const { pageId, isBookmarked, sumOfBookmarks } = pageContainer.state;
+
+    const onChangeInvoked = () => {
+      if (pageContainer.retrieveBookmarkInfo == null) { logger.error('retrieveBookmarkInfo is null') }
+      else { pageContainer.retrieveBookmarkInfo() }
+    };
 
 
     return (
     return (
       <>
       <>
@@ -27,7 +36,12 @@ const SubnavButtons = (props) => {
           </span>
           </span>
         )}
         )}
         <span>
         <span>
-          <BookmarkButton />
+          <BookmarkButton
+            pageId={pageId}
+            isBookmarked={isBookmarked}
+            sumOfBookmarks={sumOfBookmarks}
+            onChangeInvoked={onChangeInvoked}
+          />
         </span>
         </span>
       </>
       </>
     );
     );

+ 9 - 1
packages/app/src/components/Page/PageManagement.jsx

@@ -22,7 +22,9 @@ const PageManagement = (props) => {
   const {
   const {
     t, appContainer, pageContainer, isCompactMode,
     t, appContainer, pageContainer, isCompactMode,
   } = props;
   } = props;
-  const { path, isDeletable, isAbleToDeleteCompletely } = pageContainer.state;
+  const {
+    pageId, revisionId, path, isDeletable, isAbleToDeleteCompletely,
+  } = pageContainer.state;
 
 
   const { currentUser } = appContainer;
   const { currentUser } = appContainer;
   const isTopPagePath = isTopPage(path);
   const isTopPagePath = isTopPage(path);
@@ -165,11 +167,15 @@ const PageManagement = (props) => {
         <PageRenameModal
         <PageRenameModal
           isOpen={isPageRenameModalShown}
           isOpen={isPageRenameModalShown}
           onClose={closePageRenameModalHandler}
           onClose={closePageRenameModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           path={path}
         />
         />
         <PageDuplicateModal
         <PageDuplicateModal
           isOpen={isPageDuplicateModalShown}
           isOpen={isPageDuplicateModalShown}
           onClose={closePageDuplicateModalHandler}
           onClose={closePageDuplicateModalHandler}
+          pageId={pageId}
+          path={path}
         />
         />
         <CreateTemplateModal
         <CreateTemplateModal
           isOpen={isPageTemplateModalShown}
           isOpen={isPageTemplateModalShown}
@@ -178,6 +184,8 @@ const PageManagement = (props) => {
         <PageDeleteModal
         <PageDeleteModal
           isOpen={isPageDeleteModalShown}
           isOpen={isPageDeleteModalShown}
           onClose={closePageDeleteModalHandler}
           onClose={closePageDeleteModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           path={path}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
         />
         />

+ 4 - 1
packages/app/src/components/Page/TrashPageAlert.jsx

@@ -15,7 +15,7 @@ import PageDeleteModal from '../PageDeleteModal';
 const TrashPageAlert = (props) => {
 const TrashPageAlert = (props) => {
   const { t, pageContainer } = props;
   const { t, pageContainer } = props;
   const {
   const {
-    path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
+    pageId, revisionId, path, isDeleted, lastUpdateUsername, updatedAt, deletedUserName, deletedAt, isAbleToDeleteCompletely,
   } = pageContainer.state;
   } = pageContainer.state;
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isEmptyTrashModalShown, setIsEmptyTrashModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
   const [isPutbackPageModalShown, setIsPutbackPageModalShown] = useState(false);
@@ -92,11 +92,14 @@ const TrashPageAlert = (props) => {
         <PutbackPageModal
         <PutbackPageModal
           isOpen={isPutbackPageModalShown}
           isOpen={isPutbackPageModalShown}
           onClose={closePutbackPageModalHandler}
           onClose={closePutbackPageModalHandler}
+          pageId={pageId}
           path={path}
           path={path}
         />
         />
         <PageDeleteModal
         <PageDeleteModal
           isOpen={isPageDeleteModalShown}
           isOpen={isPageDeleteModalShown}
           onClose={opclosePageDeleteModalHandler}
           onClose={opclosePageDeleteModalHandler}
+          pageId={pageId}
+          revisionId={revisionId}
           path={path}
           path={path}
           isDeleteCompletelyModal
           isDeleteCompletelyModal
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}
           isAbleToDeleteCompletely={isAbleToDeleteCompletely}

+ 17 - 11
packages/app/src/components/PageDeleteModal.jsx

@@ -7,8 +7,7 @@ import {
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from './UnstatedUtils';
-import PageContainer from '~/client/services/PageContainer';
+import { apiPost } from '~/client/util/apiv1-client';
 
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
@@ -27,7 +26,7 @@ const deleteIconAndKey = {
 
 
 const PageDeleteModal = (props) => {
 const PageDeleteModal = (props) => {
   const {
   const {
-    t, pageContainer, isOpen, onClose, isDeleteCompletelyModal, path, isAbleToDeleteCompletely,
+    t, isOpen, onClose, isDeleteCompletelyModal, pageId, revisionId, path, isAbleToDeleteCompletely,
   } = props;
   } = props;
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteRecursively, setIsDeleteRecursively] = useState(true);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
   const [isDeleteCompletely, setIsDeleteCompletely] = useState(isDeleteCompletelyModal && isAbleToDeleteCompletely);
@@ -50,7 +49,18 @@ const PageDeleteModal = (props) => {
     setErrs(null);
     setErrs(null);
 
 
     try {
     try {
-      const response = await pageContainer.deletePage(isDeleteRecursively, isDeleteCompletely);
+      // control flag
+      // If is it not true, Request value must be `null`.
+      const recursively = isDeleteRecursively ? true : null;
+      const completely = isDeleteCompletely ? true : null;
+
+      const response = await apiPost('/pages.remove', {
+        page_id: pageId,
+        revision_id: revisionId,
+        recursively,
+        completely,
+      });
+
       const trashPagePath = response.page.path;
       const trashPagePath = response.page.path;
       window.location.href = encodeURI(trashPagePath);
       window.location.href = encodeURI(trashPagePath);
     }
     }
@@ -133,18 +143,14 @@ const PageDeleteModal = (props) => {
   );
   );
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const PageDeleteModalWrapper = withUnstatedContainers(PageDeleteModal, [PageContainer]);
-
 PageDeleteModal.propTypes = {
 PageDeleteModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
 
 
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   isDeleteCompletelyModal: PropTypes.bool,
   isDeleteCompletelyModal: PropTypes.bool,
   isAbleToDeleteCompletely: PropTypes.bool,
   isAbleToDeleteCompletely: PropTypes.bool,
@@ -154,4 +160,4 @@ PageDeleteModal.defaultProps = {
   isDeleteCompletelyModal: false,
   isDeleteCompletelyModal: false,
 };
 };
 
 
-export default withTranslation()(PageDeleteModalWrapper);
+export default withTranslation()(PageDeleteModal);

+ 7 - 5
packages/app/src/components/PageDuplicateModal.jsx

@@ -11,7 +11,6 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import PagePathAutoComplete from './PagePathAutoComplete';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ComparePathsTable from './ComparePathsTable';
 import ComparePathsTable from './ComparePathsTable';
@@ -20,11 +19,12 @@ import DuplicatePathsTable from './DuplicatedPathsTable';
 const LIMIT_FOR_LIST = 10;
 const LIMIT_FOR_LIST = 10;
 
 
 const PageDuplicateModal = (props) => {
 const PageDuplicateModal = (props) => {
-  const { t, appContainer, pageContainer } = props;
+  const {
+    t, appContainer, pageId, path,
+  } = props;
 
 
   const config = appContainer.getConfig();
   const config = appContainer.getConfig();
   const isReachable = config.isSearchServiceReachable;
   const isReachable = config.isSearchServiceReachable;
-  const { pageId, path } = pageContainer.state;
   const { crowi } = appContainer.config;
   const { crowi } = appContainer.config;
 
 
   const [pageNameInput, setPageNameInput] = useState(path);
   const [pageNameInput, setPageNameInput] = useState(path);
@@ -213,16 +213,18 @@ const PageDuplicateModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PageDuplicateModallWrapper = withUnstatedContainers(PageDuplicateModal, [AppContainer, PageContainer]);
+const PageDuplicateModallWrapper = withUnstatedContainers(PageDuplicateModal, [AppContainer]);
 
 
 
 
 PageDuplicateModal.propTypes = {
 PageDuplicateModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
+
+  pageId: PropTypes.string.isRequired,
+  path: PropTypes.string.isRequired,
 };
 };
 
 
 export default withTranslation()(PageDuplicateModallWrapper);
 export default withTranslation()(PageDuplicateModallWrapper);

+ 1 - 1
packages/app/src/components/PageList/Page.jsx

@@ -11,7 +11,7 @@ export default class Page extends React.Component {
       page, noLink,
       page, noLink,
     } = this.props;
     } = this.props;
 
 
-    let pagePathElem = <PagePathLabel page={page} additionalClassNames={['mx-1']} />;
+    let pagePathElem = <PagePathLabel path={page.path} additionalClassNames={['mx-1']} />;
     if (!noLink) {
     if (!noLink) {
       pagePathElem = <a className="text-break" href={page.path}>{pagePathElem}</a>;
       pagePathElem = <a className="text-break" href={page.path}>{pagePathElem}</a>;
     }
     }

+ 25 - 22
packages/app/src/components/PageRenameModal.jsx

@@ -14,7 +14,9 @@ import { withUnstatedContainers } from './UnstatedUtils';
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 
 
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-import PageContainer from '~/client/services/PageContainer';
+
+import { apiv3Get, apiv3Put } from '~/client/util/apiv3-client';
+
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ComparePathsTable from './ComparePathsTable';
 import ComparePathsTable from './ComparePathsTable';
 import DuplicatedPathsTable from './DuplicatedPathsTable';
 import DuplicatedPathsTable from './DuplicatedPathsTable';
@@ -22,11 +24,9 @@ import DuplicatedPathsTable from './DuplicatedPathsTable';
 
 
 const PageRenameModal = (props) => {
 const PageRenameModal = (props) => {
   const {
   const {
-    t, appContainer, pageContainer,
+    t, appContainer, path, pageId, revisionId,
   } = props;
   } = props;
 
 
-  const { path } = pageContainer.state;
-
   const { crowi } = appContainer.config;
   const { crowi } = appContainer.config;
 
 
   const [pageNameInput, setPageNameInput] = useState(path);
   const [pageNameInput, setPageNameInput] = useState(path);
@@ -37,7 +37,7 @@ const PageRenameModal = (props) => {
   const [existingPaths, setExistingPaths] = useState([]);
   const [existingPaths, setExistingPaths] = useState([]);
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRecursively, SetIsRenameRecursively] = useState(true);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
   const [isRenameRedirect, SetIsRenameRedirect] = useState(false);
-  const [isRenameMetadata, SetIsRenameMetadata] = useState(false);
+  const [isRemainMetadata, SetIsRemainMetadata] = useState(false);
   const [subordinatedError] = useState(null);
   const [subordinatedError] = useState(null);
   const [isRenameRecursivelyWithoutExistPath, setIsRenameRecursivelyWithoutExistPath] = useState(true);
   const [isRenameRecursivelyWithoutExistPath, setIsRenameRecursivelyWithoutExistPath] = useState(true);
 
 
@@ -53,13 +53,13 @@ const PageRenameModal = (props) => {
     SetIsRenameRedirect(!isRenameRedirect);
     SetIsRenameRedirect(!isRenameRedirect);
   }
   }
 
 
-  function changeIsRenameMetadataHandler() {
-    SetIsRenameMetadata(!isRenameMetadata);
+  function changeIsRemainMetadataHandler() {
+    SetIsRemainMetadata(!isRemainMetadata);
   }
   }
 
 
   const updateSubordinatedList = useCallback(async() => {
   const updateSubordinatedList = useCallback(async() => {
     try {
     try {
-      const res = await appContainer.apiv3Get('/pages/subordinated-list', { path });
+      const res = await apiv3Get('/pages/subordinated-list', { path });
       const { subordinatedPaths } = res.data;
       const { subordinatedPaths } = res.data;
       setSubordinatedPages(subordinatedPaths);
       setSubordinatedPages(subordinatedPaths);
     }
     }
@@ -67,7 +67,7 @@ const PageRenameModal = (props) => {
       setErrs(err);
       setErrs(err);
       toastError(t('modal_rename.label.Fail to get subordinated pages'));
       toastError(t('modal_rename.label.Fail to get subordinated pages'));
     }
     }
-  }, [appContainer, path, t]);
+  }, [path, t]);
 
 
   useEffect(() => {
   useEffect(() => {
     if (props.isOpen) {
     if (props.isOpen) {
@@ -78,7 +78,7 @@ const PageRenameModal = (props) => {
 
 
   const checkExistPaths = async(newParentPath) => {
   const checkExistPaths = async(newParentPath) => {
     try {
     try {
-      const res = await appContainer.apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
+      const res = await apiv3Get('/page/exist-paths', { fromPath: path, toPath: newParentPath });
       const { existPaths } = res.data;
       const { existPaths } = res.data;
       setExistingPaths(existPaths);
       setExistingPaths(existPaths);
     }
     }
@@ -112,12 +112,15 @@ const PageRenameModal = (props) => {
     setErrs(null);
     setErrs(null);
 
 
     try {
     try {
-      const response = await pageContainer.rename(
-        pageNameInput,
-        isRenameRecursively,
+      const response = await apiv3Put('/pages/rename', {
+        revisionId,
+        pageId,
+        isRecursively: isRenameRecursively,
         isRenameRedirect,
         isRenameRedirect,
-        isRenameMetadata,
-      );
+        isRemainMetadata,
+        newPagePath: pageNameInput,
+        path,
+      });
 
 
       const { page } = response.data;
       const { page } = response.data;
       const url = new URL(page.path, 'https://dummy');
       const url = new URL(page.path, 'https://dummy');
@@ -215,12 +218,12 @@ const PageRenameModal = (props) => {
           <input
           <input
             className="custom-control-input"
             className="custom-control-input"
             name="remain_metadata"
             name="remain_metadata"
-            id="cbRenameMetadata"
+            id="cbRemainMetadata"
             type="checkbox"
             type="checkbox"
-            checked={isRenameMetadata}
-            onChange={changeIsRenameMetadataHandler}
+            checked={isRemainMetadata}
+            onChange={changeIsRemainMetadataHandler}
           />
           />
-          <label className="custom-control-label" htmlFor="cbRenameMetadata">
+          <label className="custom-control-label" htmlFor="cbRemainMetadata">
             { t('modal_rename.label.Do not update metadata') }
             { t('modal_rename.label.Do not update metadata') }
             <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
             <p className="form-text text-muted mt-0">{ t('modal_rename.help.metadata') }</p>
           </label>
           </label>
@@ -244,17 +247,17 @@ const PageRenameModal = (props) => {
 /**
 /**
  * Wrapper component for using unstated
  * Wrapper component for using unstated
  */
  */
-const PageRenameModalWrapper = withUnstatedContainers(PageRenameModal, [AppContainer, PageContainer]);
-
+const PageRenameModalWrapper = withUnstatedContainers(PageRenameModal, [AppContainer]);
 
 
 PageRenameModal.propTypes = {
 PageRenameModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
 
 
+  pageId: PropTypes.string.isRequired,
+  revisionId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
 };
 };
 
 

+ 13 - 12
packages/app/src/components/PutbackPageModal.jsx

@@ -7,15 +7,13 @@ import {
 
 
 import { withTranslation } from 'react-i18next';
 import { withTranslation } from 'react-i18next';
 
 
-import { withUnstatedContainers } from './UnstatedUtils';
-
-import PageContainer from '~/client/services/PageContainer';
+import { apiPost } from '~/client/util/apiv1-client';
 
 
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 import ApiErrorMessageList from './PageManagement/ApiErrorMessageList';
 
 
 const PutBackPageModal = (props) => {
 const PutBackPageModal = (props) => {
   const {
   const {
-    t, isOpen, onClose, pageContainer, path,
+    t, isOpen, onClose, pageId, path,
   } = props;
   } = props;
 
 
   const [errs, setErrs] = useState(null);
   const [errs, setErrs] = useState(null);
@@ -30,7 +28,15 @@ const PutBackPageModal = (props) => {
     setErrs(null);
     setErrs(null);
 
 
     try {
     try {
-      const response = await pageContainer.revertRemove(isPutbackRecursively);
+      // control flag
+      // If is it not true, Request value must be `null`.
+      const recursively = isPutbackRecursively ? true : null;
+
+      const response = await apiPost('/pages.revertRemove', {
+        page_id: pageId,
+        recursively,
+      });
+
       const putbackPagePath = response.page.path;
       const putbackPagePath = response.page.path;
       window.location.href = encodeURI(putbackPagePath);
       window.location.href = encodeURI(putbackPagePath);
     }
     }
@@ -80,20 +86,15 @@ const PutBackPageModal = (props) => {
 
 
 };
 };
 
 
-/**
- * Wrapper component for using unstated
- */
-const PutBackPageModalWrapper = withUnstatedContainers(PutBackPageModal, [PageContainer]);
-
 PutBackPageModal.propTypes = {
 PutBackPageModal.propTypes = {
   t: PropTypes.func.isRequired, //  i18next
   t: PropTypes.func.isRequired, //  i18next
-  pageContainer: PropTypes.instanceOf(PageContainer).isRequired,
 
 
   isOpen: PropTypes.bool.isRequired,
   isOpen: PropTypes.bool.isRequired,
   onClose: PropTypes.func.isRequired,
   onClose: PropTypes.func.isRequired,
 
 
+  pageId: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
   path: PropTypes.string.isRequired,
 };
 };
 
 
 
 
-export default withTranslation()(PutBackPageModalWrapper);
+export default withTranslation()(PutBackPageModal);

+ 33 - 29
packages/app/src/components/SearchPage.jsx

@@ -7,7 +7,6 @@ import toastr from 'toastr';
 
 
 import { withUnstatedContainers } from './UnstatedUtils';
 import { withUnstatedContainers } from './UnstatedUtils';
 import AppContainer from '~/client/services/AppContainer';
 import AppContainer from '~/client/services/AppContainer';
-
 import { toastError } from '~/client/util/apiNotification';
 import { toastError } from '~/client/util/apiNotification';
 import SearchPageLayout from './SearchPage/SearchPageLayout';
 import SearchPageLayout from './SearchPage/SearchPageLayout';
 import SearchResultContent from './SearchPage/SearchResultContent';
 import SearchResultContent from './SearchPage/SearchResultContent';
@@ -41,13 +40,13 @@ class SearchPage extends React.Component {
     this.state = {
     this.state = {
       searchingKeyword: decodeURI(this.props.query.q) || '',
       searchingKeyword: decodeURI(this.props.query.q) || '',
       searchedKeyword: '',
       searchedKeyword: '',
-      searchedPages: [],
+      searchResults: [],
       searchResultMeta: {},
       searchResultMeta: {},
-      focusedPage: {},
+      focusedSearchResultData: null,
       selectedPagesIdList: new Set(),
       selectedPagesIdList: new Set(),
       searchResultCount: 0,
       searchResultCount: 0,
       activePage: 1,
       activePage: 1,
-      pagingLimit: 10, // change to an appropriate limit number
+      pagingLimit: this.props.appContainer.config.pageLimitationL,
       excludeUsersHome: true,
       excludeUsersHome: true,
       excludeTrash: true,
       excludeTrash: true,
       selectAllCheckboxType: CheckboxType.NONE_CHECKED,
       selectAllCheckboxType: CheckboxType.NONE_CHECKED,
@@ -55,12 +54,13 @@ class SearchPage extends React.Component {
 
 
     this.changeURL = this.changeURL.bind(this);
     this.changeURL = this.changeURL.bind(this);
     this.search = this.search.bind(this);
     this.search = this.search.bind(this);
-    this.searchHandler = this.searchHandler.bind(this);
+    this.onSearchInvoked = this.onSearchInvoked.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.selectPage = this.selectPage.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
     this.toggleCheckBox = this.toggleCheckBox.bind(this);
     this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
     this.onExcludeUsersHome = this.onExcludeUsersHome.bind(this);
     this.onExcludeTrash = this.onExcludeTrash.bind(this);
     this.onExcludeTrash = this.onExcludeTrash.bind(this);
     this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
     this.onPagingNumberChanged = this.onPagingNumberChanged.bind(this);
+    this.onPagingLimitChanged = this.onPagingLimitChanged.bind(this);
     this.deleteSelectedPages = this.deleteSelectedPages.bind(this);
     this.deleteSelectedPages = this.deleteSelectedPages.bind(this);
     this.onClickDeleteAllButton = this.onClickDeleteAllButton.bind(this);
     this.onClickDeleteAllButton = this.onClickDeleteAllButton.bind(this);
     this.onCloseDeleteConfirmModal = this.onCloseDeleteConfirmModal.bind(this);
     this.onCloseDeleteConfirmModal = this.onCloseDeleteConfirmModal.bind(this);
@@ -123,27 +123,30 @@ class SearchPage extends React.Component {
    * this method is called when user changes paging number
    * this method is called when user changes paging number
    */
    */
   async onPagingNumberChanged(activePage) {
   async onPagingNumberChanged(activePage) {
-    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
-    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
     this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
     this.setState({ activePage }, () => this.search({ keyword: this.state.searchedKeyword }));
   }
   }
 
 
   /**
   /**
    * this method is called when user searches by pressing Enter or using searchbox
    * this method is called when user searches by pressing Enter or using searchbox
    */
    */
-  async searchHandler(data) {
-    // this.setState does not change the state immediately and following calls of this.search outside of this.setState will have old activePage state.
-    // To prevent above, pass this.search as a callback function to make sure this.search will have the latest activePage state.
+  async onSearchInvoked(data) {
     this.setState({ activePage: 1 }, () => this.search(data));
     this.setState({ activePage: 1 }, () => this.search(data));
   }
   }
 
 
+  /**
+   * change number of pages to display per page and execute search method after.
+   */
+  async onPagingLimitChanged(limit) {
+    this.setState({ pagingLimit: limit }, () => this.search({ keyword: this.state.searchedKeyword }));
+  }
+
   async search(data) {
   async search(data) {
     const keyword = data.keyword;
     const keyword = data.keyword;
     if (keyword === '') {
     if (keyword === '') {
       this.setState({
       this.setState({
         searchingKeyword: '',
         searchingKeyword: '',
         searchedKeyword: '',
         searchedKeyword: '',
-        searchedPages: [],
+        searchResults: [],
         searchResultMeta: {},
         searchResultMeta: {},
         searchResultCount: 0,
         searchResultCount: 0,
         activePage: 1,
         activePage: 1,
@@ -167,10 +170,10 @@ class SearchPage extends React.Component {
       if (res.data.length > 0) {
       if (res.data.length > 0) {
         this.setState({
         this.setState({
           searchedKeyword: keyword,
           searchedKeyword: keyword,
-          searchedPages: res.data,
+          searchResults: res.data,
           searchResultMeta: res.meta,
           searchResultMeta: res.meta,
           searchResultCount: res.meta.total,
           searchResultCount: res.meta.total,
-          focusedPage: res.data[0],
+          focusedSearchResultData: res.data[0],
           // reset active page if keyword changes, otherwise set the current state
           // reset active page if keyword changes, otherwise set the current state
           activePage: this.state.searchedKeyword === keyword ? this.state.activePage : 1,
           activePage: this.state.searchedKeyword === keyword ? this.state.activePage : 1,
         });
         });
@@ -178,10 +181,10 @@ class SearchPage extends React.Component {
       else {
       else {
         this.setState({
         this.setState({
           searchedKeyword: keyword,
           searchedKeyword: keyword,
-          searchedPages: [],
+          searchResults: [],
           searchResultMeta: {},
           searchResultMeta: {},
           searchResultCount: 0,
           searchResultCount: 0,
-          focusedPage: {},
+          focusedSearchResultData: {},
           activePage: 1,
           activePage: 1,
         });
         });
       }
       }
@@ -192,11 +195,11 @@ class SearchPage extends React.Component {
   }
   }
 
 
   selectPage= (pageId) => {
   selectPage= (pageId) => {
-    const index = this.state.searchedPages.findIndex((page) => {
-      return page._id === pageId;
+    const index = this.state.searchResults.findIndex(({ pageData }) => {
+      return pageData._id === pageId;
     });
     });
     this.setState({
     this.setState({
-      focusedPage: this.state.searchedPages[index],
+      focusedSearchResultData: this.state.searchResults[index],
     });
     });
   }
   }
 
 
@@ -212,7 +215,7 @@ class SearchPage extends React.Component {
     switch (selectedPagesIdList.size) {
     switch (selectedPagesIdList.size) {
       case 0:
       case 0:
         return this.setState({ selectAllCheckboxType: CheckboxType.NONE_CHECKED });
         return this.setState({ selectAllCheckboxType: CheckboxType.NONE_CHECKED });
-      case this.state.searchedPages.length:
+      case this.state.searchResults.length:
         return this.setState({ selectAllCheckboxType: CheckboxType.ALL_CHECKED });
         return this.setState({ selectAllCheckboxType: CheckboxType.ALL_CHECKED });
       default:
       default:
         return this.setState({ selectAllCheckboxType: CheckboxType.INDETERMINATE });
         return this.setState({ selectAllCheckboxType: CheckboxType.INDETERMINATE });
@@ -220,13 +223,13 @@ class SearchPage extends React.Component {
   }
   }
 
 
   toggleAllCheckBox = (nextSelectAllCheckboxType) => {
   toggleAllCheckBox = (nextSelectAllCheckboxType) => {
-    const { selectedPagesIdList, searchedPages } = this.state;
+    const { selectedPagesIdList, searchResults } = this.state;
     if (nextSelectAllCheckboxType === CheckboxType.NONE_CHECKED) {
     if (nextSelectAllCheckboxType === CheckboxType.NONE_CHECKED) {
       selectedPagesIdList.clear();
       selectedPagesIdList.clear();
     }
     }
     else {
     else {
-      searchedPages.forEach((page) => {
-        selectedPagesIdList.add(page._id);
+      searchResults.forEach((page) => {
+        selectedPagesIdList.add(page.pageData._id);
       });
       });
     }
     }
     this.setState({
     this.setState({
@@ -236,8 +239,8 @@ class SearchPage extends React.Component {
   };
   };
 
 
   getSelectedPages() {
   getSelectedPages() {
-    return this.state.searchedPages.filter((page) => {
-      return Array.from(this.state.selectedPagesIdList).find(id => id === page.id);
+    return this.state.searchResults.filter((page) => {
+      return Array.from(this.state.selectedPagesIdList).find(id => id === page.pageData._id);
     });
     });
   }
   }
 
 
@@ -290,7 +293,7 @@ class SearchPage extends React.Component {
       <SearchResultContent
       <SearchResultContent
         appContainer={this.props.appContainer}
         appContainer={this.props.appContainer}
         searchingKeyword={this.state.searchingKeyword}
         searchingKeyword={this.state.searchingKeyword}
-        focusedPage={this.state.focusedPage}
+        focusedSearchResultData={this.state.focusedSearchResultData}
       >
       >
       </SearchResultContent>
       </SearchResultContent>
     );
     );
@@ -299,8 +302,8 @@ class SearchPage extends React.Component {
   renderSearchResultList = () => {
   renderSearchResultList = () => {
     return (
     return (
       <SearchResultList
       <SearchResultList
-        pages={this.state.searchedPages || []}
-        focusedPage={this.state.focusedPage}
+        pages={this.state.searchResults || []}
+        focusedSearchResultData={this.state.focusedSearchResultData}
         selectedPagesIdList={this.state.selectedPagesIdList || []}
         selectedPagesIdList={this.state.selectedPagesIdList || []}
         searchResultCount={this.state.searchResultCount}
         searchResultCount={this.state.searchResultCount}
         activePage={this.state.activePage}
         activePage={this.state.activePage}
@@ -318,7 +321,7 @@ class SearchPage extends React.Component {
         searchingKeyword={this.state.searchingKeyword}
         searchingKeyword={this.state.searchingKeyword}
         searchResultCount={this.state.searchResultCount || 0}
         searchResultCount={this.state.searchResultCount || 0}
         appContainer={this.props.appContainer}
         appContainer={this.props.appContainer}
-        onSearchInvoked={this.searchHandler}
+        onSearchInvoked={this.onSearchInvoked}
         onExcludeUsersHome={this.onExcludeUsersHome}
         onExcludeUsersHome={this.onExcludeUsersHome}
         onExcludeTrash={this.onExcludeTrash}
         onExcludeTrash={this.onExcludeTrash}
         onClickSelectAllCheckbox={this.toggleAllCheckBox}
         onClickSelectAllCheckbox={this.toggleAllCheckBox}
@@ -338,6 +341,8 @@ class SearchPage extends React.Component {
           SearchResultContent={this.renderSearchResultContent}
           SearchResultContent={this.renderSearchResultContent}
           searchResultMeta={this.state.searchResultMeta}
           searchResultMeta={this.state.searchResultMeta}
           searchingKeyword={this.state.searchedKeyword}
           searchingKeyword={this.state.searchedKeyword}
+          onPagingLimitChanged={this.onPagingLimitChanged}
+          initialPagingLimit={this.props.appContainer.config.pageLimitationL || 50}
         >
         >
         </SearchPageLayout>
         </SearchPageLayout>
         <DeletePageListModal
         <DeletePageListModal
@@ -363,7 +368,6 @@ const SearchPageWrapper = withUnstatedContainers(SearchPage, [AppContainer]);
 SearchPage.propTypes = {
 SearchPage.propTypes = {
   t: PropTypes.func.isRequired, // i18next
   t: PropTypes.func.isRequired, // i18next
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
-
   query: PropTypes.object,
   query: PropTypes.object,
 };
 };
 SearchPage.defaultProps = {
 SearchPage.defaultProps = {

+ 2 - 1
packages/app/src/components/SearchPage/DeletePageListModal.jsx

@@ -23,7 +23,7 @@ class DeletePageListModal extends React.Component {
 
 
     const listView = this.props.pages.map((page) => {
     const listView = this.props.pages.map((page) => {
       return (
       return (
-        <li key={page._id}>{page.path}</li>
+        <li key={page.pageData._id}>{page.pageData.path}</li>
       );
       );
     });
     });
 
 
@@ -48,6 +48,7 @@ class DeletePageListModal extends React.Component {
                   id="customCheck-delete-completely"
                   id="customCheck-delete-completely"
                   checked={this.props.isDeleteCompletely}
                   checked={this.props.isDeleteCompletely}
                   onChange={this.props.onChangeDeleteCompletely}
                   onChange={this.props.onChangeDeleteCompletely}
+                  disabled
                 />
                 />
                 <label
                 <label
                   className="custom-control-label text-danger"
                   className="custom-control-label text-danger"

+ 4 - 3
packages/app/src/components/SearchPage/DeleteSelectedPageGroup.tsx

@@ -27,7 +27,8 @@ const DeleteSelectedPageGroup:FC<Props> = (props:Props) => {
   };
   };
 
 
   return (
   return (
-    <>
+
+    <div className="d-flex align-items-center">
       {/** todo: implement the design for CheckboxType = INDETERMINATE */}
       {/** todo: implement the design for CheckboxType = INDETERMINATE */}
       <input
       <input
         id="check-all-pages"
         id="check-all-pages"
@@ -40,14 +41,14 @@ const DeleteSelectedPageGroup:FC<Props> = (props:Props) => {
       />
       />
       <button
       <button
         type="button"
         type="button"
-        className="btn text-danger font-weight-light p-0 ml-3"
+        className="btn text-danger font-weight-light p-0 ml-2"
         disabled={selectAllCheckboxType === CheckboxType.NONE_CHECKED}
         disabled={selectAllCheckboxType === CheckboxType.NONE_CHECKED}
         onClick={onClickDeleteButton}
         onClick={onClickDeleteButton}
       >
       >
         <i className="icon-trash"></i>
         <i className="icon-trash"></i>
         {t('search_result.delete_all_selected_page')}
         {t('search_result.delete_all_selected_page')}
       </button>
       </button>
-    </>
+    </div>
   );
   );
 
 
 };
 };

+ 37 - 27
packages/app/src/components/SearchPage/SearchControl.tsx

@@ -37,13 +37,19 @@ const SearchControl: FC <Props> = (props: Props) => {
   };
   };
 
 
   return (
   return (
-    <div className="">
-      <div className="search-page-input sps sps--abv">
-        <SearchPageFormTypeAny
-          keyword={props.searchingKeyword}
-          appContainer={props.appContainer}
-          onSearchFormChanged={props.onSearchInvoked}
-        />
+    <>
+      <div className="search-page-nav d-flex py-3 align-items-center">
+        <div className="flex-grow-1 mx-4">
+          <SearchPageFormTypeAny
+            keyword={props.searchingKeyword}
+            appContainer={props.appContainer}
+            onSearchFormChanged={props.onSearchInvoked}
+          />
+        </div>
+        <div className="mr-4">
+          {/* TODO: replace the following button */}
+          <button type="button">related pages</button>
+        </div>
       </div>
       </div>
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       {/* TODO: replace the following elements deleteAll button , relevance button and include specificPath button component */}
       <div className="d-flex my-4">
       <div className="d-flex my-4">
@@ -54,28 +60,32 @@ const SearchControl: FC <Props> = (props: Props) => {
           onClickDeleteAllButton={props.onClickDeleteAllButton}
           onClickDeleteAllButton={props.onClickDeleteAllButton}
           onClickSelectAllCheckbox={props.onClickSelectAllCheckbox}
           onClickSelectAllCheckbox={props.onClickSelectAllCheckbox}
         />
         />
-        <div className="d-flex align-items-center border rounded border-gray px-2 py-1 mr-2 ml-auto">
-          <label className="my-0 mr-2" htmlFor="flexCheckDefault">
-            {t('Include Subordinated Target Page', { target: '/user' })}
-          </label>
-          <input
-            type="checkbox"
-            id="flexCheckDefault"
-            onClick={() => onExcludeUsersHome()}
-          />
-        </div>
-        <div className="d-flex align-items-center border rounded border-gray px-2 mr-3">
-          <label className="my-0 mr-2" htmlFor="flexCheckChecked">
-            {t('Include Subordinated Target Page', { target: '/trash' })}
-          </label>
-          <input
-            type="checkbox"
-            id="flexCheckChecked"
-            onClick={() => onExcludeTrash()}
-          />
+        <div className="d-flex align-items-center rounded border-gray px-2 py-1 mr-2 ml-auto">
+          <div className="border border-gray mr-3">
+            <label className="px-3 py-2 mb-0 d-flex align-items-center" htmlFor="flexCheckDefault">
+              <input
+                className="mr-2"
+                type="checkbox"
+                id="flexCheckDefault"
+                onClick={() => onExcludeUsersHome()}
+              />
+              {t('Include Subordinated Target Page', { target: '/user' })}
+            </label>
+          </div>
+          <div className="border border-gray">
+            <label className="px-3 py-2 mb-0 d-flex align-items-center" htmlFor="flexCheckChecked">
+              <input
+                className="mr-2"
+                type="checkbox"
+                id="flexCheckChecked"
+                onClick={() => onExcludeTrash()}
+              />
+              {t('Include Subordinated Target Page', { target: '/trash' })}
+            </label>
+          </div>
         </div>
         </div>
       </div>
       </div>
-    </div>
+    </>
   );
   );
 };
 };
 
 

+ 17 - 18
packages/app/src/components/SearchPage/SearchPageForm.jsx

@@ -41,30 +41,29 @@ class SearchPageForm extends React.Component {
   render() {
   render() {
     return (
     return (
       // TODO: modify design after other component is created
       // TODO: modify design after other component is created
-      <div className="grw-search-form-in-search-result-page d-flex">
+      <div className="grw-search-form-in-search-result-page d-flex align-items-center">
         <div className="input-group flex-nowrap">
         <div className="input-group flex-nowrap">
           <SearchForm
           <SearchForm
             onSubmit={this.search}
             onSubmit={this.search}
             keyword={this.state.searchedKeyword}
             keyword={this.state.searchedKeyword}
             onInputChange={this.onInputChange}
             onInputChange={this.onInputChange}
           />
           />
-        </div>
-        <div className="input-group-append">
-          <button
-            className="btn btn-secondary"
-            type="button"
-            id="button-addon2"
-            onClick={() => {
-              try {
-                this.search();
-              }
-              catch (error) {
-                logger.error(error);
-              }
-            }}
-          >
-            <i className="icon-magnifier"></i>
-          </button>
+          <div className="btn-group-submit-search">
+            <button
+              className="btn border-0 pb-1"
+              type="button"
+              onClick={() => {
+                try {
+                  this.search();
+                }
+                catch (error) {
+                  logger.error(error);
+                }
+              }}
+            >
+              <i className="pr-2 icon-magnifier"></i>
+            </button>
+          </div>
         </div>
         </div>
       </div>
       </div>
     );
     );

+ 28 - 7
packages/app/src/components/SearchPage/SearchPageLayout.tsx

@@ -1,4 +1,5 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
+import { useTranslation } from 'react-i18next';
 
 
 type SearchResultMeta = {
 type SearchResultMeta = {
   took : number,
   took : number,
@@ -11,19 +12,39 @@ type Props = {
   SearchResultList: React.FunctionComponent,
   SearchResultList: React.FunctionComponent,
   SearchResultContent: React.FunctionComponent,
   SearchResultContent: React.FunctionComponent,
   searchResultMeta: SearchResultMeta,
   searchResultMeta: SearchResultMeta,
-  searchingKeyword: string
+  searchingKeyword: string,
+  initialPagingLimit: number,
+  onPagingLimitChanged: (limit: number) => void
 }
 }
 
 
 const SearchPageLayout: FC<Props> = (props: Props) => {
 const SearchPageLayout: FC<Props> = (props: Props) => {
-  const { SearchResultList, SearchControl, SearchResultContent } = props;
+  const { t } = useTranslation('');
+  const {
+    SearchResultList, SearchControl, SearchResultContent, searchResultMeta, searchingKeyword,
+  } = props;
+
   return (
   return (
     <div className="content-main">
     <div className="content-main">
       <div className="search-result row" id="search-result">
       <div className="search-result row" id="search-result">
-        <div className="col-xl-6  page-list search-result-list pr-0" id="search-result-list">
+        <div className="col-lg-6  page-list border boder-gray search-result-list px-0" id="search-result-list">
+
           <nav><SearchControl></SearchControl></nav>
           <nav><SearchControl></SearchControl></nav>
-          <div className="d-flex align-items-start justify-content-between mt-1">
-            <div className="search-result-meta">
-              <i className="icon-magnifier" /> Found {props.searchResultMeta.total} pages with &quot;{props.searchingKeyword}&quot;
+          <div className="d-flex align-items-center justify-content-between mt-1 mb-3">
+            <div className="search-result-meta text-nowrap mr-3">
+              <span className="font-weight-light">{t('search_result.result_meta')} </span>
+              <span className="h5">{`"${searchingKeyword}"`}</span>
+              {/* Todo: replace "1-10" to the appropriate value */}
+              <span className="ml-3">1-10 / {searchResultMeta.total || 0}</span>
+            </div>
+            <div className="input-group search-result-select-group">
+              <div className="input-group-prepend">
+                <label className="input-group-text text-secondary" htmlFor="inputGroupSelect01">{t('search_result.number_of_list_to_display')}</label>
+              </div>
+              <select className="custom-select" id="inputGroupSelect01" onChange={(e) => { props.onPagingLimitChanged(Number(e.target.value)) }}>
+                {[20, 50, 100, 200].map((limit) => {
+                  return <option selected={limit === props.initialPagingLimit} value={limit}>{limit}{t('search_result.page_number_unit')}</option>;
+                })}
+              </select>
             </div>
             </div>
           </div>
           </div>
 
 
@@ -31,7 +52,7 @@ const SearchPageLayout: FC<Props> = (props: Props) => {
             <ul className="page-list-ul page-list-ul-flat nav nav-pills"><SearchResultList></SearchResultList></ul>
             <ul className="page-list-ul page-list-ul-flat nav nav-pills"><SearchResultList></SearchResultList></ul>
           </div>
           </div>
         </div>
         </div>
-        <div className="col-xl-6 d-none d-lg-block search-result-content">
+        <div className="col-lg-6 d-none d-lg-block search-result-content">
           <SearchResultContent></SearchResultContent>
           <SearchResultContent></SearchResultContent>
         </div>
         </div>
       </div>
       </div>

+ 28 - 31
packages/app/src/components/SearchPage/SearchResultContent.tsx

@@ -1,5 +1,7 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
 
 
+import { IPageSearchResultData } from '../../interfaces/search';
+
 import RevisionLoader from '../Page/RevisionLoader';
 import RevisionLoader from '../Page/RevisionLoader';
 import AppContainer from '../../client/services/AppContainer';
 import AppContainer from '../../client/services/AppContainer';
 
 
@@ -7,42 +9,37 @@ import AppContainer from '../../client/services/AppContainer';
 type Props ={
 type Props ={
   appContainer: AppContainer,
   appContainer: AppContainer,
   searchingKeyword:string,
   searchingKeyword:string,
-  focusedPage : any,
+  focusedSearchResultData : IPageSearchResultData,
 }
 }
 const SearchResultContent: FC<Props> = (props: Props) => {
 const SearchResultContent: FC<Props> = (props: Props) => {
+  const page = props.focusedSearchResultData?.pageData || {};
+  if (page == null) return null;
   // Temporaly workaround for lint error
   // Temporaly workaround for lint error
   // later needs to be fixed: RevisoinRender to typescriptcomponet
   // later needs to be fixed: RevisoinRender to typescriptcomponet
-  const RevisionRenderTypeAny: any = RevisionLoader;
-  const renderPage = (page) => {
-    const growiRenderer = props.appContainer.getRenderer('searchresult');
-    let showTags = false;
-    if (page.tags != null && page.tags.length > 0) { showTags = true }
-    return (
-      <div key={page._id} className="search-result-page mb-5">
-        <h2>
-          <a href={page.path} className="text-break">
-            {page.path}
-          </a>
-          {showTags && (
-            <div className="mt-1 small">
-              <i className="tag-icon icon-tag"></i> {page.tags.join(', ')}
-            </div>
-          )}
-        </h2>
-        <RevisionRenderTypeAny
-          growiRenderer={growiRenderer}
-          pageId={page._id}
-          pagePath={page.path}
-          revisionId={page.revision}
-          highlightKeywords={props.searchingKeyword}
-        />
-      </div>
-    );
-  };
-  const content = renderPage(props.focusedPage);
+  const RevisionLoaderTypeAny: any = RevisionLoader;
+  const growiRenderer = props.appContainer.getRenderer('searchresult');
+  let showTags = false;
+  if (page.tags != null && page.tags.length > 0) { showTags = true }
   return (
   return (
-
-    <div>{content}</div>
+    <div key={page._id} className="search-result-page mb-5">
+      <h2>
+        <a href={page.path} className="text-break">
+          {page.path}
+        </a>
+        {showTags && (
+          <div className="mt-1 small">
+            <i className="tag-icon icon-tag"></i> {page.tags?.join(', ')}
+          </div>
+        )}
+      </h2>
+      <RevisionLoaderTypeAny
+        growiRenderer={growiRenderer}
+        pageId={page._id}
+        pagePath={page.path}
+        revisionId={page.revision}
+        highlightKeywords={props.searchingKeyword}
+      />
+    </div>
   );
   );
 };
 };
 
 

+ 12 - 18
packages/app/src/components/SearchPage/SearchResultList.tsx

@@ -1,45 +1,39 @@
 import React, { FC } from 'react';
 import React, { FC } from 'react';
 import SearchResultListItem from './SearchResultListItem';
 import SearchResultListItem from './SearchResultListItem';
-import { IPageHasId } from '../../interfaces/page';
 import PaginationWrapper from '../PaginationWrapper';
 import PaginationWrapper from '../PaginationWrapper';
+import { IPageSearchResultData } from '../../interfaces/search';
 
 
-// TOOD: retrieve bookmark count and add it to the following type
-export type ISearchedPage = IPageHasId & {
-  snippet: string,
-  elasticSearchResult: {
-    snippet: string,
-    matchedPath: string,
-  },
-};
 
 
 type Props = {
 type Props = {
-  pages: ISearchedPage[],
+  pages: IPageSearchResultData[],
   selectedPagesIdList: Set<string>
   selectedPagesIdList: Set<string>
-  onClickSearchResultItem?: (pageId: string) => void,
-  onClickCheckbox?: (pageId: string) => void,
   searchResultCount?: number,
   searchResultCount?: number,
   activePage?: number,
   activePage?: number,
   pagingLimit?: number,
   pagingLimit?: number,
+  focusedSearchResultData?: IPageSearchResultData,
   onPagingNumberChanged?: (activePage: number) => void,
   onPagingNumberChanged?: (activePage: number) => void,
-  focusedPage?: ISearchedPage,
+  onClickSearchResultItem?: (pageId: string) => void,
+  onClickCheckbox?: (pageId: string) => void,
+  onClickInvoked?: (pageId: string) => void,
 }
 }
 
 
 const SearchResultList: FC<Props> = (props:Props) => {
 const SearchResultList: FC<Props> = (props:Props) => {
-  const { focusedPage, selectedPagesIdList } = props;
-  const focusedPageId = focusedPage != null && focusedPage._id != null ? focusedPage._id : '';
+  const { focusedSearchResultData, selectedPagesIdList } = props;
 
 
+  const focusedPageId = (focusedSearchResultData != null && focusedSearchResultData.pageData != null) ? focusedSearchResultData.pageData._id : '';
   return (
   return (
     <>
     <>
       {Array.isArray(props.pages) && props.pages.map((page) => {
       {Array.isArray(props.pages) && props.pages.map((page) => {
-        const isChecked = selectedPagesIdList.has(page._id);
+        const isChecked = selectedPagesIdList.has(page.pageData._id);
+
         return (
         return (
           <SearchResultListItem
           <SearchResultListItem
-            key={page._id}
+            key={page.pageData._id}
             page={page}
             page={page}
             onClickSearchResultItem={props.onClickSearchResultItem}
             onClickSearchResultItem={props.onClickSearchResultItem}
             onClickCheckbox={props.onClickCheckbox}
             onClickCheckbox={props.onClickCheckbox}
-            isSelected={page._id === focusedPageId || false}
             isChecked={isChecked}
             isChecked={isChecked}
+            isSelected={page.pageData._id === focusedPageId || false}
           />
           />
         );
         );
       })}
       })}

+ 18 - 26
packages/app/src/components/SearchPage/SearchResultListItem.tsx

@@ -6,17 +6,15 @@ import toastr from 'toastr';
 import { useTranslation } from 'react-i18next';
 import { useTranslation } from 'react-i18next';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { UserPicture, PageListMeta, PagePathLabel } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
-import { ISearchedPage } from './SearchResultList';
+import { IPageSearchResultData } from '../../interfaces/search';
 
 
-import loggerFactory from '~/utils/logger';
-
-const logger = loggerFactory('growi:searchResultList');
+import { IPageHasId } from '~/interfaces/page';
 
 
 type PageItemControlProps = {
 type PageItemControlProps = {
-  page: ISearchedPage,
+  page: IPageHasId,
 }
 }
 
 
-const PageItemControl: FC<PageItemControlProps> = (props: {page: ISearchedPage}) => {
+const PageItemControl: FC<PageItemControlProps> = (props: {page: IPageHasId}) => {
 
 
   const { page } = props;
   const { page } = props;
   const { t } = useTranslation('');
   const { t } = useTranslation('');
@@ -68,7 +66,7 @@ const PageItemControl: FC<PageItemControlProps> = (props: {page: ISearchedPage})
 };
 };
 
 
 type Props = {
 type Props = {
-  page: ISearchedPage,
+  page: IPageSearchResultData,
   isSelected: boolean,
   isSelected: boolean,
   isChecked: boolean,
   isChecked: boolean,
   onClickCheckbox?: (pageId: string) => void,
   onClickCheckbox?: (pageId: string) => void,
@@ -78,25 +76,22 @@ type Props = {
 const SearchResultListItem: FC<Props> = (props:Props) => {
 const SearchResultListItem: FC<Props> = (props:Props) => {
   const {
   const {
     // todo: refactoring variable name to clear what changed
     // todo: refactoring variable name to clear what changed
-    page, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked,
+    page: { pageData, pageMeta }, isSelected, onClickSearchResultItem, onClickCheckbox, isChecked,
   } = props;
   } = props;
 
 
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
   // Add prefix 'id_' in pageId, because scrollspy of bootstrap doesn't work when the first letter of id attr of target component is numeral.
-  const pageId = `#${page._id}`;
+  const pageId = `#${pageData._id}`;
 
 
-  const dPagePath = new DevidedPagePath(page.path, false, true);
-  const pagePathElem = <PagePathLabel page={page} isFormerOnly />;
+  const isPathIncludedHtml = pageMeta.elasticSearchResult.highlightedPath != null;
+  const dPagePath = new DevidedPagePath(pageData.path, false, true);
+  const pagePathElem = <PagePathLabel path={pageMeta.elasticSearchResult.highlightedPath} isFormerOnly isPathIncludedHtml={isPathIncludedHtml} />;
 
 
   return (
   return (
-    <li key={page._id} className={`page-list-li search-page-item w-100 border-bottom pr-4 list-group-item-action ${isSelected ? 'active' : ''}`}>
+    <li key={pageData._id} className={`page-list-li search-page-item w-100 border-bottom px-4 list-group-item-action ${isSelected ? 'active' : ''}`}>
       <a
       <a
         className="d-block pt-3"
         className="d-block pt-3"
         href={pageId}
         href={pageId}
-        onClick={() => {
-          if (onClickSearchResultItem != null) {
-            onClickSearchResultItem(page._id);
-          }
-        }}
+        onClick={() => onClickSearchResultItem != null && onClickSearchResultItem(pageData._id)}
       >
       >
         <div className="d-flex">
         <div className="d-flex">
           {/* checkbox */}
           {/* checkbox */}
@@ -105,9 +100,9 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
               className="form-check-input my-auto"
               className="form-check-input my-auto"
               type="checkbox"
               type="checkbox"
               id="flexCheckDefault"
               id="flexCheckDefault"
-              onClick={() => {
+              onChange={() => {
                 if (onClickCheckbox != null) {
                 if (onClickCheckbox != null) {
-                  onClickCheckbox(page._id);
+                  onClickCheckbox(pageData._id);
                 }
                 }
               }}
               }}
               checked={isChecked}
               checked={isChecked}
@@ -122,26 +117,23 @@ const SearchResultListItem: FC<Props> = (props:Props) => {
             <div className="d-flex my-1 align-items-center">
             <div className="d-flex my-1 align-items-center">
               {/* page title */}
               {/* page title */}
               <h3 className="mb-0">
               <h3 className="mb-0">
-                <UserPicture user={page.lastUpdateUser} />
+                <UserPicture user={pageData.lastUpdateUser} />
                 <span className="mx-2">{dPagePath.latter}</span>
                 <span className="mx-2">{dPagePath.latter}</span>
               </h3>
               </h3>
               {/* page meta */}
               {/* page meta */}
               <div className="d-flex mx-2">
               <div className="d-flex mx-2">
-                <PageListMeta page={page} />
+                <PageListMeta page={pageData} bookmarkCount={pageMeta.bookmarkCount} />
               </div>
               </div>
               {/* doropdown icon includes page control buttons */}
               {/* doropdown icon includes page control buttons */}
               <div className="ml-auto">
               <div className="ml-auto">
-                <PageItemControl page={page} />
+                <PageItemControl page={pageData} />
               </div>
               </div>
             </div>
             </div>
             <div className="my-2">
             <div className="my-2">
               <Clamp
               <Clamp
                 lines={2}
                 lines={2}
               >
               >
-                {page.snippet
-                  ? <div className="mt-1">page.snippet</div>
-                  : <div className="mt-1" dangerouslySetInnerHTML={{ __html: page.elasticSearchResult.snippet }}></div>
-                }
+                {pageMeta.elasticSearchResult && <div className="mt-1" dangerouslySetInnerHTML={{ __html: pageMeta.elasticSearchResult.snippet }}></div>}
               </Clamp>
               </Clamp>
             </div>
             </div>
           </div>
           </div>

+ 1 - 1
packages/app/src/components/SearchTypeahead.jsx

@@ -180,7 +180,7 @@ class SearchTypeahead extends React.Component {
     return (
     return (
       <span>
       <span>
         <UserPicture user={page.lastUpdateUser} size="sm" noLink />
         <UserPicture user={page.lastUpdateUser} size="sm" noLink />
-        <span className="ml-1 text-break text-wrap"><PagePathLabel page={page} /></span>
+        <span className="ml-1 text-break text-wrap"><PagePathLabel path={page.path} /></span>
         <PageListMeta page={page} />
         <PageListMeta page={page} />
       </span>
       </span>
     );
     );

+ 1 - 3
packages/app/src/components/Sidebar/RecentChanges.jsx

@@ -5,7 +5,7 @@ import PropTypes from 'prop-types';
 
 
 import { useTranslation, withTranslation } from 'react-i18next';
 import { useTranslation, withTranslation } from 'react-i18next';
 
 
-import { UserPicture } from '@growi/ui';
+import { UserPicture, FootstampIcon } from '@growi/ui';
 import { DevidedPagePath } from '@growi/core';
 import { DevidedPagePath } from '@growi/core';
 
 
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
 import PagePathHierarchicalLink from '~/components/PagePathHierarchicalLink';
@@ -16,8 +16,6 @@ import loggerFactory from '~/utils/logger';
 
 
 import LinkedPagePath from '~/models/linked-page-path';
 import LinkedPagePath from '~/models/linked-page-path';
 
 
-import FootstampIcon from '../FootstampIcon';
-
 
 
 import FormattedDistanceDate from '../FormattedDistanceDate';
 import FormattedDistanceDate from '../FormattedDistanceDate';
 
 

+ 1 - 1
packages/app/src/components/User/SeenUserInfo.jsx

@@ -5,13 +5,13 @@ import React, { useState } from 'react';
 import {
 import {
   Button, Popover, PopoverBody,
   Button, Popover, PopoverBody,
 } from 'reactstrap';
 } from 'reactstrap';
+import { FootstampIcon } from '@growi/ui';
 import UserPictureList from './UserPictureList';
 import UserPictureList from './UserPictureList';
 
 
 import { withUnstatedContainers } from '../UnstatedUtils';
 import { withUnstatedContainers } from '../UnstatedUtils';
 
 
 import PageContainer from '~/client/services/PageContainer';
 import PageContainer from '~/client/services/PageContainer';
 
 
-import FootstampIcon from '../FootstampIcon';
 
 
 /* eslint react/no-multi-comp: 0, react/prop-types: 0 */
 /* eslint react/no-multi-comp: 0, react/prop-types: 0 */
 
 

+ 13 - 0
packages/app/src/interfaces/search.ts

@@ -1,5 +1,18 @@
+import { IPageHasId } from './page';
+
 export enum CheckboxType {
 export enum CheckboxType {
   NONE_CHECKED = 'noneChecked',
   NONE_CHECKED = 'noneChecked',
   INDETERMINATE = 'indeterminate',
   INDETERMINATE = 'indeterminate',
   ALL_CHECKED = 'allChecked',
   ALL_CHECKED = 'allChecked',
 }
 }
+
+export type IPageSearchResultData = {
+  pageData: IPageHasId,
+  pageMeta: {
+    bookmarkCount: number,
+    elasticSearchResult: {
+      snippet: string,
+      highlightedPath: string,
+    },
+  },
+}

+ 1 - 0
packages/app/src/server/models/config.ts

@@ -235,6 +235,7 @@ schema.statics.getLocalconfig = function(crowi) {
     isSearchServiceReachable: crowi.searchService.isReachable,
     isSearchServiceReachable: crowi.searchService.isReachable,
     isMailerSetup: crowi.mailService.isMailerSetup,
     isMailerSetup: crowi.mailService.isMailerSetup,
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
     globalLang: crowi.configManager.getConfig('crowi', 'app:globalLang'),
+    pageLimitationL: crowi.configManager.getConfig('crowi', 'customize:showPageLimitationL'),
   };
   };
 
 
   return localConfig;
   return localConfig;

+ 22 - 12
packages/app/src/server/routes/search.js

@@ -152,25 +152,36 @@ module.exports = function(crowi, app) {
 
 
       const ids = esResult.data.map((page) => { return page._id });
       const ids = esResult.data.map((page) => { return page._id });
       const findResult = await Page.findListByPageIds(ids);
       const findResult = await Page.findListByPageIds(ids);
-      // add tags and elasticSearch data to page
-      findResult.pages.map((page) => {
+
+      // add tags data to page
+      findResult.pages.map((pageData) => {
         const data = esResult.data.find((data) => {
         const data = esResult.data.find((data) => {
-          return page.id === data._id;
+          return pageData.id === data._id;
         });
         });
-        page._doc.tags = data._source.tag_names;
-        page._doc.elasticSearchResult = data.elasticSearchResult;
-        return page;
+        pageData._doc.tags = data._source.tag_names;
+        return pageData;
       });
       });
 
 
       result.meta = esResult.meta;
       result.meta = esResult.meta;
       result.totalCount = findResult.totalCount;
       result.totalCount = findResult.totalCount;
       result.data = findResult.pages
       result.data = findResult.pages
-        .map((page) => {
-          if (page.lastUpdateUser != null && page.lastUpdateUser instanceof User) {
-            page.lastUpdateUser = serializeUserSecurely(page.lastUpdateUser);
+        .map((pageData) => {
+          if (pageData.lastUpdateUser != null && pageData.lastUpdateUser instanceof User) {
+            pageData.lastUpdateUser = serializeUserSecurely(pageData.lastUpdateUser);
           }
           }
-          page.bookmarkCount = (page._source && page._source.bookmark_count) || 0;
-          return page;
+
+          const data = esResult.data.find((data) => {
+            return pageData.id === data._id;
+          });
+
+          const pageMeta = {
+            bookmarkCount: data._source.bookmark_count || 0,
+            elasticSearchResult: data.elasticSearchResult,
+          };
+
+          pageData._doc.seenUserCount = (pageData.seenUsers && pageData.seenUsers.length) || 0;
+
+          return { pageData, pageMeta };
         })
         })
         .sort((page1, page2) => {
         .sort((page1, page2) => {
           // note: this do not consider NaN
           // note: this do not consider NaN
@@ -180,7 +191,6 @@ module.exports = function(crowi, app) {
     catch (err) {
     catch (err) {
       return res.json(ApiResponse.error(err));
       return res.json(ApiResponse.error(err));
     }
     }
-
     return res.json(ApiResponse.success(result));
     return res.json(ApiResponse.success(result));
   };
   };
 
 

+ 0 - 6
packages/app/src/server/service/config-manager.ts

@@ -18,13 +18,7 @@ const KEYS_FOR_SAML_USE_ONLY_ENV_OPTION = [
   'security:passport-saml:isEnabled',
   'security:passport-saml:isEnabled',
   'security:passport-saml:entryPoint',
   'security:passport-saml:entryPoint',
   'security:passport-saml:issuer',
   'security:passport-saml:issuer',
-  'security:passport-saml:attrMapId',
-  'security:passport-saml:attrMapUsername',
-  'security:passport-saml:attrMapMail',
-  'security:passport-saml:attrMapFirstName',
-  'security:passport-saml:attrMapLastName',
   'security:passport-saml:cert',
   'security:passport-saml:cert',
-  'security:passport-saml:ABLCRule',
 ];
 ];
 
 
 const KEYS_FOR_FIEL_UPLOAD_USE_ONLY_ENV_OPTION = [
 const KEYS_FOR_FIEL_UPLOAD_USE_ONLY_ENV_OPTION = [

+ 16 - 3
packages/app/src/server/service/passport.ts

@@ -810,15 +810,16 @@ class PassportService implements S2sMessageHandlable {
     }
     }
 
 
     const { field, term } = luceneRule;
     const { field, term } = luceneRule;
-    if (field === '<implicit>') {
+    const unescapedField = this.literalUnescape(field);
+    if (unescapedField === '<implicit>') {
       return attributes[term] != null;
       return attributes[term] != null;
     }
     }
 
 
-    if (attributes[field] == null) {
+    if (attributes[unescapedField] == null) {
       return false;
       return false;
     }
     }
 
 
-    return attributes[field].includes(term);
+    return attributes[unescapedField].includes(term);
   }
   }
 
 
   /**
   /**
@@ -973,6 +974,18 @@ class PassportService implements S2sMessageHandlable {
     return this.crowi.configManager.getConfig('crowi', key);
     return this.crowi.configManager.getConfig('crowi', key);
   }
   }
 
 
+  literalUnescape(string: string) {
+    return string
+      .replace(/\\\\/g, '\\')
+      .replace(/\\\//g, '/')
+      .replace(/\\:/g, ':')
+      .replace(/\\"/g, '"')
+      .replace(/\\0/g, '\0')
+      .replace(/\\t/g, '\t')
+      .replace(/\\n/g, '\n')
+      .replace(/\\r/g, '\r');
+  }
+
 }
 }
 
 
 module.exports = PassportService;
 module.exports = PassportService;

+ 1 - 2
packages/app/src/server/service/search.js

@@ -170,8 +170,7 @@ class SearchService {
 
 
       data.elasticSearchResult = {
       data.elasticSearchResult = {
         snippet: filterXss.process(snippet),
         snippet: filterXss.process(snippet),
-        // todo: use filter xss.process() for matchedPath;
-        matchedPath: pathMatch,
+        highlightedPath: filterXss.process(pathMatch),
       };
       };
     });
     });
     return esResult;
     return esResult;

+ 23 - 38
packages/app/src/server/service/slack-command-handler/note.js

@@ -1,8 +1,9 @@
 import loggerFactory from '~/utils/logger';
 import loggerFactory from '~/utils/logger';
 
 
 const {
 const {
-  markdownSectionBlock, inputSectionBlock, inputBlock,
+  markdownHeaderBlock, inputSectionBlock, inputBlock, actionsBlock, buttonElement,
 } = require('@growi/slack');
 } = require('@growi/slack');
+const { SlackCommandHandlerError } = require('../../models/vo/slack-command-handler-error');
 
 
 const logger = loggerFactory('growi:service:SlackCommandHandler:note');
 const logger = loggerFactory('growi:service:SlackCommandHandler:note');
 
 
@@ -14,58 +15,42 @@ module.exports = (crowi) => {
   const conversationsSelectElement = {
   const conversationsSelectElement = {
     action_id: 'conversation',
     action_id: 'conversation',
     type: 'conversations_select',
     type: 'conversations_select',
-    response_url_enabled: true,
     default_to_current_conversation: true,
     default_to_current_conversation: true,
   };
   };
 
 
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
   handler.handleCommand = async(growiCommand, client, body, respondUtil) => {
-    await client.views.open({
-      trigger_id: body.trigger_id,
-
-      view: {
-        type: 'modal',
-        callback_id: 'note:createPage',
-        title: {
-          type: 'plain_text',
-          text: 'Take a note',
-        },
-        submit: {
-          type: 'plain_text',
-          text: 'Submit',
-        },
-        close: {
-          type: 'plain_text',
-          text: 'Cancel',
-        },
-        blocks: [
-          markdownSectionBlock('Take a note on GROWI'),
-          inputBlock(conversationsSelectElement, 'conversation', 'Channel name to display in the page to be created'),
-          inputSectionBlock('path', 'Page path', 'path_input', false, '/path'),
-          inputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
-        ],
-        private_metadata: JSON.stringify({ channelId: body.channel_id, channelName: body.channel_name }),
-      },
+    await respondUtil.respond({
+      text: 'Take a note on GROWI',
+      blocks: [
+        markdownHeaderBlock('Take a note on GROWI'),
+        inputBlock(conversationsSelectElement, 'conversation', 'Channel name to display in the page to be created'),
+        inputSectionBlock('path', 'Page path', 'path_input', false, '/path'),
+        inputSectionBlock('contents', 'Contents', 'contents_input', true, 'Input with Markdown...'),
+        actionsBlock(
+          buttonElement({ text: 'Cancel', actionId: 'note:cancel' }),
+          buttonElement({ text: 'Create page', actionId: 'note:createPage', style: 'primary' }),
+        ),
+
+      ],
     });
     });
   };
   };
 
 
+  handler.cancel = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
+    await respondUtil.deleteOriginal();
+  };
+
   handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName, respondUtil) {
   handler.handleInteractions = async function(client, interactionPayload, interactionPayloadAccessor, handlerMethodName, respondUtil) {
     await this[handlerMethodName](client, interactionPayload, interactionPayloadAccessor, respondUtil);
     await this[handlerMethodName](client, interactionPayload, interactionPayloadAccessor, respondUtil);
   };
   };
 
 
   handler.createPage = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
   handler.createPage = async function(client, interactionPayload, interactionPayloadAccessor, respondUtil) {
     const path = interactionPayloadAccessor.getStateValues()?.path.path_input.value;
     const path = interactionPayloadAccessor.getStateValues()?.path.path_input.value;
-    const privateMetadata = interactionPayloadAccessor.getViewPrivateMetaData();
-    if (privateMetadata == null) {
-      await respondUtil.respond({
-        text: 'Error occurred',
-        blocks: [
-          markdownSectionBlock('Failed to create a page.'),
-        ],
-      });
-      return;
-    }
     const contentsBody = interactionPayloadAccessor.getStateValues()?.contents.contents_input.value;
     const contentsBody = interactionPayloadAccessor.getStateValues()?.contents.contents_input.value;
+    if (path == null || contentsBody == null) {
+      throw new SlackCommandHandlerError('All parameters are required.');
+    }
     await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil);
     await createPageService.createPageInGrowi(interactionPayloadAccessor, path, contentsBody, respondUtil);
+    await respondUtil.deleteOriginal();
   };
   };
 
 
   return handler;
   return handler;

+ 1 - 1
packages/app/src/server/views/search.html

@@ -17,7 +17,7 @@
 <div class="container-fluid">
 <div class="container-fluid">
 
 
   <div class="row">
   <div class="row">
-    <div id="main" class="main col-lg-12 search-page">
+    <div id="main" class="main col-lg-12 search-page mt-0">
       <div class="" id="search-page"></div>
       <div class="" id="search-page"></div>
     </div>
     </div>
   </div>
   </div>

+ 39 - 15
packages/app/src/styles/_search.scss

@@ -1,6 +1,17 @@
-.search-listpage-icon {
-  font-size: 16px;
-  color: $gray-400;
+.search-page-nav {
+  background-color: #f7f7f7;
+}
+
+.search-group-submit-button {
+  position: absolute;
+  top: 0;
+  right: 0;
+  z-index: 3;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  width: 32px;
+  height: 32px;
 }
 }
 
 
 .search-listpage-clear {
 .search-listpage-clear {
@@ -102,17 +113,19 @@
   }
   }
 
 
   .btn-group-submit-search {
   .btn-group-submit-search {
-    position: absolute;
-    top: 0;
-    right: 0;
+    @extend .search-group-submit-button;
+  }
+}
 
 
-    z-index: 3;
+.grw-search-form-in-search-result-page {
+  .btn-group-submit-search {
+    @extend .search-group-submit-button;
+  }
 
 
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    width: 32px;
-    height: 32px;
+  button {
+    &:focus {
+      box-shadow: none !important;
+    }
   }
   }
 }
 }
 
 
@@ -158,7 +171,7 @@
 .search-result {
 .search-result {
   .search-result-list {
   .search-result-list {
     position: sticky;
     position: sticky;
-    top: 64px;
+    top: 0px;
     height: 100vh;
     height: 100vh;
     overflow-y: scroll;
     overflow-y: scroll;
 
 
@@ -182,14 +195,25 @@
             margin-right: 3px;
             margin-right: 3px;
           }
           }
         }
         }
+        .page-list-meta {
+          > span {
+            margin-right: 12px;
+          }
+          .footstamp-icon {
+            margin-right: 2px;
+          }
+        }
       }
       }
     }
     }
 
 
     .search-result-meta {
     .search-result-meta {
-      margin-bottom: 10px;
       font-weight: bold;
       font-weight: bold;
     }
     }
-
+    .search-result-select-group {
+      > select {
+        max-width: 8rem;
+      }
+    }
     .search-result-list-delete-checkbox {
     .search-result-list-delete-checkbox {
       margin: 0 10px 0 0;
       margin: 0 10px 0 0;
       vertical-align: middle;
       vertical-align: middle;

+ 8 - 1
packages/app/src/styles/theme/_apply-colors.scss

@@ -18,7 +18,8 @@ $color-seen-user: #549c79 !default;
 $color-btn-reload-in-sidebar: $gray-500;
 $color-btn-reload-in-sidebar: $gray-500;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 $bgcolor-keyword-highlighted: $grw-marker-yellow !default;
 $bordercolor-search-item-left-active: $primary;
 $bordercolor-search-item-left-active: $primary;
-$bgcolor-search-item-active: lighten($bordercolor-search-item-left-active, 76%);
+$bgcolor-search-item-active: lighten($bordercolor-search-item-left-active, 76%) !default;
+$color-search-item-pagelist-meta: $gray-500 !default;
 
 
 // override bootstrap variables
 // override bootstrap variables
 $body-bg: $bgcolor-global;
 $body-bg: $bgcolor-global;
@@ -600,6 +601,12 @@ body.pathname-sidebar {
           }
           }
         }
         }
       }
       }
+      .page-list-meta {
+        color: $color-search-item-pagelist-meta;
+        svg {
+          fill: $color-search-item-pagelist-meta;
+        }
+      }
     }
     }
   }
   }
 }
 }

+ 4 - 1
packages/app/tsconfig.base.json

@@ -2,5 +2,8 @@
   "extends": "../../tsconfig.base.json",
   "extends": "../../tsconfig.base.json",
   "compilerOptions": {
   "compilerOptions": {
     "jsx": "react"
     "jsx": "react"
-  }
+  },
+  "include": [
+    "src/**/*"
+  ],
 }
 }

+ 1 - 1
packages/codemirror-textlint/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/codemirror-textlint",
   "name": "@growi/codemirror-textlint",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "scripts": {
   "scripts": {

+ 1 - 1
packages/core/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/core",
   "name": "@growi/core",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "description": "GROWI Core Libraries",
   "description": "GROWI Core Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 2 - 2
packages/core/src/models/devided-page-path.js

@@ -2,8 +2,8 @@ import * as pathUtils from '../utils/path-utils';
 
 
 // https://regex101.com/r/BahpKX/2
 // https://regex101.com/r/BahpKX/2
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
 const PATTERN_INCLUDE_DATE = /^(.+\/[^/]+)\/(\d{4}|\d{4}\/\d{2}|\d{4}\/\d{2}\/\d{2})$/;
-// https://regex101.com/r/WVpPpY/1
-const PATTERN_DEFAULT = /^((.*)\/)?([^/]+)$/;
+// https://regex101.com/r/HJNvMW/1
+const PATTERN_DEFAULT = /^((.*)(?<!<)\/)?(.+)$/;
 
 
 export class DevidedPagePath {
 export class DevidedPagePath {
 
 

+ 1 - 1
packages/plugin-attachment-refs/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/plugin-attachment-refs",
   "name": "@growi/plugin-attachment-refs",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "description": "GROWI Plugin to add ref/refimg/refs/refsimg tags",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/plugin-lsx/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/plugin-lsx",
   "name": "@growi/plugin-lsx",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "description": "GROWI plugin to list pages",
   "description": "GROWI plugin to list pages",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/plugin-lsx/src/client/js/components/LsxPageList/PagePathWrapper.jsx

@@ -13,7 +13,7 @@ export class PagePathWrapper extends React.Component {
     }
     }
 
 
     return (
     return (
-      <PagePathLabel page={{ path: this.props.pagePath }} isLatterOnly additionalClassNames={classNames} />
+      <PagePathLabel path={this.props.pagePath} isLatterOnly additionalClassNames={classNames} />
     );
     );
   }
   }
 
 

+ 1 - 1
packages/plugin-pukiwiki-like-linker/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/plugin-pukiwiki-like-linker",
   "name": "@growi/plugin-pukiwiki-like-linker",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "description": "GROWI plugin to add PukiwikiLikeLinker",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 1 - 1
packages/slack/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slack",
   "name": "@growi/slack",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "license": "MIT",
   "license": "MIT",
   "main": "dist/index.js",
   "main": "dist/index.js",
   "typings": "dist/index.d.ts",
   "typings": "dist/index.d.ts",

+ 2 - 2
packages/slackbot-proxy/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/slackbot-proxy",
   "name": "@growi/slackbot-proxy",
-  "version": "4.4.11-slackbot-proxy.0",
+  "version": "4.4.13-slackbot-proxy.0",
   "license": "MIT",
   "license": "MIT",
   "scripts": {
   "scripts": {
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
     "build": "yarn tsc && tsc-alias -p tsconfig.build.json",
@@ -25,7 +25,7 @@
   },
   },
   "dependencies": {
   "dependencies": {
     "@godaddy/terminus": "^4.9.0",
     "@godaddy/terminus": "^4.9.0",
-    "@growi/slack": "^4.4.11-RC.0",
+    "@growi/slack": "^4.4.13-RC.0",
     "@slack/oauth": "^2.0.1",
     "@slack/oauth": "^2.0.1",
     "@slack/web-api": "^6.2.4",
     "@slack/web-api": "^6.2.4",
     "@tsed/common": "^6.43.0",
     "@tsed/common": "^6.43.0",

+ 1 - 1
packages/ui/package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "@growi/ui",
   "name": "@growi/ui",
-  "version": "4.4.11-RC.0",
+  "version": "4.4.13-RC.0",
   "description": "GROWI UI Libraries",
   "description": "GROWI UI Libraries",
   "license": "MIT",
   "license": "MIT",
   "keywords": [
   "keywords": [

+ 19 - 3
packages/ui/src/components/PagePath/PageListMeta.jsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import React from 'react';
 import PropTypes from 'prop-types';
 import PropTypes from 'prop-types';
 import { templateChecker, pagePathUtils } from '@growi/core';
 import { templateChecker, pagePathUtils } from '@growi/core';
+import { FootstampIcon } from '../SearchPage/FootstampIcon';
 
 
 const { isTopPage } = pagePathUtils;
 const { isTopPage } = pagePathUtils;
 const { checkTemplatePath } = templateChecker;
 const { checkTemplatePath } = templateChecker;
@@ -37,13 +38,30 @@ export class PageListMeta extends React.Component {
       locked = <span><i className="icon-lock" /></span>;
       locked = <span><i className="icon-lock" /></span>;
     }
     }
 
 
+    let seenUserCount;
+    if (page.seenUserCount > 0) {
+      seenUserCount = (
+        <span>
+          <i className="footstamp-icon"><FootstampIcon /></i>
+          {page.seenUsers.length}
+        </span>
+      );
+    }
+
+    let bookmarkCount;
+    if (this.props.bookmarkCount > 0) {
+      bookmarkCount = <span><i className="icon-star" />{this.props.bookmarkCount}</span>;
+    }
+
     return (
     return (
       <span className="page-list-meta">
       <span className="page-list-meta">
         {topLabel}
         {topLabel}
         {templateLabel}
         {templateLabel}
+        {seenUserCount}
         {commentCount}
         {commentCount}
         {likerCount}
         {likerCount}
         {locked}
         {locked}
+        {bookmarkCount}
       </span>
       </span>
     );
     );
   }
   }
@@ -52,7 +70,5 @@ export class PageListMeta extends React.Component {
 
 
 PageListMeta.propTypes = {
 PageListMeta.propTypes = {
   page: PropTypes.object.isRequired,
   page: PropTypes.object.isRequired,
-};
-
-PageListMeta.defaultProps = {
+  bookmarkCount: PropTypes.number,
 };
 };

+ 0 - 40
packages/ui/src/components/PagePath/PagePathLabel.jsx

@@ -1,40 +0,0 @@
-import React from 'react';
-import PropTypes from 'prop-types';
-
-import { DevidedPagePath } from '@growi/core';
-
-export const PagePathLabel = (props) => {
-
-  const dPagePath = new DevidedPagePath(props.page.path, false, true);
-
-  let classNames = [''];
-  classNames = classNames.concat(props.additionalClassNames);
-
-  if (props.isLatterOnly) {
-    return <span className={classNames.join(' ')}>{dPagePath.latter}</span>;
-  }
-
-  if (props.isFormerOnly) {
-    const textElem = dPagePath.isFormerRoot
-      ? <>/</>
-      : <>{dPagePath.former}</>;
-    return <span className={classNames.join(' ')}>{textElem}</span>;
-  }
-
-  const textElem = dPagePath.isRoot
-    ? <><strong>/</strong></>
-    : <>{dPagePath.former}/<strong>{dPagePath.latter}</strong></>;
-
-  return <span className={classNames.join(' ')}>{textElem}</span>;
-};
-
-PagePathLabel.propTypes = {
-  page: PropTypes.object.isRequired,
-  isLatterOnly: PropTypes.bool,
-  isFormerOnly: PropTypes.bool,
-  additionalClassNames: PropTypes.arrayOf(PropTypes.string),
-};
-
-PagePathLabel.defaultProps = {
-  additionalClassNames: [],
-};

+ 56 - 0
packages/ui/src/components/PagePath/PagePathLabel.tsx

@@ -0,0 +1,56 @@
+import React, { FC } from 'react';
+
+import { DevidedPagePath } from '@growi/core';
+
+
+type TextElemProps = {
+  children?: React.ReactNode
+  isHTML?: boolean,
+}
+
+const TextElement: FC<TextElemProps> = (props: TextElemProps) => (
+  <>
+    { props.isHTML
+      // eslint-disable-next-line react/no-danger
+      ? <span dangerouslySetInnerHTML={{ __html: props.children?.toString() || '' }}></span>
+      : <>{props.children}</>
+    }
+  </>
+);
+
+
+type Props = {
+  path: string,
+  isLatterOnly?: boolean,
+  isFormerOnly?: boolean,
+  isPathIncludedHtml?: boolean,
+  additionalClassNames?: string[],
+}
+
+export const PagePathLabel: FC<Props> = (props:Props) => {
+  const {
+    isLatterOnly, isFormerOnly, isPathIncludedHtml, additionalClassNames, path,
+  } = props;
+
+  const dPagePath = new DevidedPagePath(path, false, true);
+
+  const classNames = additionalClassNames || [];
+
+  let textElem;
+
+  if (isLatterOnly) {
+    textElem = <TextElement isHTML={isPathIncludedHtml}>{dPagePath.latter}</TextElement>;
+  }
+  else if (isFormerOnly) {
+    textElem = dPagePath.isFormerRoot
+      ? <>/</>
+      : <TextElement isHTML={isPathIncludedHtml}>{dPagePath.former}</TextElement>;
+  }
+  else {
+    textElem = dPagePath.isRoot
+      ? <strong>/</strong>
+      : <TextElement isHTML={isPathIncludedHtml}>{dPagePath.former}/<strong>{dPagePath.latter}</strong></TextElement>;
+  }
+
+  return <span className={classNames.join(' ')}>{textElem}</span>;
+};

+ 1 - 3
packages/app/src/components/FootstampIcon.jsx → packages/ui/src/components/SearchPage/FootstampIcon.jsx

@@ -1,6 +1,6 @@
 import React from 'react';
 import React from 'react';
 
 
-const FootstampIcon = () => (
+export const FootstampIcon = () => (
   <svg
   <svg
     xmlns="http://www.w3.org/2000/svg"
     xmlns="http://www.w3.org/2000/svg"
     width="16"
     width="16"
@@ -27,5 +27,3 @@ const FootstampIcon = () => (
     <path d="M13.49,7.57a.81.81,0,0,0-.8.71l-.1.71a.82.82,0,0,0,.7.91h.11a.81.81,0,0,0,.8-.71l.1-.71a.81.81,0,0,0-.7-.91Z" />
     <path d="M13.49,7.57a.81.81,0,0,0-.8.71l-.1.71a.82.82,0,0,0,.7.91h.11a.81.81,0,0,0,.8-.71l.1-.71a.81.81,0,0,0-.7-.91Z" />
   </svg>
   </svg>
 );
 );
-
-export default FootstampIcon;

+ 1 - 0
packages/ui/src/index.ts

@@ -2,3 +2,4 @@ export * from './components/Attachment/Attachment';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PageListMeta';
 export * from './components/PagePath/PagePathLabel';
 export * from './components/PagePath/PagePathLabel';
 export * from './components/User/UserPicture';
 export * from './components/User/UserPicture';
+export * from './components/SearchPage/FootstampIcon';

+ 4 - 4
yarn.lock

@@ -15472,10 +15472,10 @@ passport-twitter@^1.0.4:
     passport-oauth1 "1.x.x"
     passport-oauth1 "1.x.x"
     xtraverse "0.1.x"
     xtraverse "0.1.x"
 
 
-passport@^0.5.0:
-  version "0.5.0"
-  resolved "https://registry.yarnpkg.com/passport/-/passport-0.5.0.tgz#7914aaa55844f9dce8c3aa28f7d6b73647ee0169"
-  integrity sha512-ln+ue5YaNDS+fes6O5PCzXKSseY5u8MYhX9H5Co4s+HfYI5oqvnHKoOORLYDUPh+8tHvrxugF2GFcUA1Q1Gqfg==
+passport@^0.4.0:
+  version "0.4.1"
+  resolved "https://registry.yarnpkg.com/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270"
+  integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg==
   dependencies:
   dependencies:
     passport-strategy "1.x.x"
     passport-strategy "1.x.x"
     pause "0.0.1"
     pause "0.0.1"