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

Merge branch 'master' into reactify-admin/security

# Conflicts:
#	resource/locales/en-US/translation.json
#	resource/locales/ja/translation.json
itizawa 6 лет назад
Родитель
Сommit
284c744b06

+ 3 - 2
.github/workflows/build.yml

@@ -1,8 +1,9 @@
 name: Release Docker Images
 name: Release Docker Images
 
 
 on:
 on:
-  release:
-    types: [published]
+  push:
+    tags:
+      - v3.*.*
 
 
 jobs:
 jobs:
 
 

+ 1 - 1
.github/workflows/release.yml

@@ -35,7 +35,7 @@ jobs:
         git commit -am "Release v${{ env.RELEASE_VERSION }}"
         git commit -am "Release v${{ env.RELEASE_VERSION }}"
         git tag -a v${{ env.RELEASE_VERSION }} -m "v${{ env.RELEASE_VERSION }}"
         git tag -a v${{ env.RELEASE_VERSION }} -m "v${{ env.RELEASE_VERSION }}"
         git push --follow-tags origin $TMP_RELEASE_BRANCH
         git push --follow-tags origin $TMP_RELEASE_BRANCH
-        git push --delete origin ${{ env.TMP_RELEASE_BRANCH }}
+        git push --delete origin $TMP_RELEASE_BRANCH
 
 
     - name: Upload release notes
     - name: Upload release notes
       uses: Roang-zero1/github-create-release-action@master
       uses: Roang-zero1/github-create-release-action@master

+ 1 - 2
.stylelintrc.json

@@ -1,7 +1,6 @@
 {
 {
   "extends": [
   "extends": [
-    "stylelint-config-recess-order",
-    "./node_modules/prettier-stylelint/config.js"
+    "stylelint-config-recess-order"
   ],
   ],
   "ignoreFiles": [
   "ignoreFiles": [
     "src/client/styles/scss/_override-bootstrap-variables.scss",
     "src/client/styles/scss/_override-bootstrap-variables.scss",

+ 14 - 6
.vscode/settings.json

@@ -6,15 +6,23 @@
     "javascript": "jsx"
     "javascript": "jsx"
   },
   },
 
 
+  // use stylelint-plus
+  // see https://qiita.com/y-w/items/bd7f11013fe34b69f0df#vs-code%E3%81%A8%E7%B5%84%E3%81%BF%E5%90%88%E3%82%8F%E3%81%9B%E3%82%8B
+  "css.validate": false,
+  "scss.validate": false,
+  "[css]": {
+    "editor.formatOnSave": true
+  },
+  "[scss]": {
+    "editor.formatOnSave": true
+  },
+  "stylelint.autoFixOnSave": true,
+
   // for vscode-eslint
   // for vscode-eslint
-  "eslint.autoFixOnSave": true,
   "[javascript]": {
   "[javascript]": {
     "editor.formatOnSave": false
     "editor.formatOnSave": false
   },
   },
-
-  // for prettier-vecode + prettier-stylelint
-  "prettier.stylelintIntegration": true,
-  "[scss]": {
-    "editor.formatOnSave": true
+  "editor.codeActionsOnSave": {
+    "source.fixAll.eslint": true
   }
   }
 }
 }

+ 8 - 1
CHANGES.md

@@ -1,6 +1,13 @@
 # CHANGES
 # CHANGES
 
 
-## 3.6.2-RC
+## 3.6.3-RC
+
+* Improvement: Searching users in UserGroup Management
+* Fix: Repair google authentication by migrating to jaredhanson/passport-google-oauth2
+* Fix: Markdown Settings are broken by import recommended button
+
+
+## 3.6.2
 
 
 * Improvement: Reactify admin pages (Customize)
 * Improvement: Reactify admin pages (Customize)
 * Improvement: Ensure not to consider `[text|site](https://example.com]` as a row in the table
 * Improvement: Ensure not to consider `[text|site](https://example.com]` as a row in the table

+ 1 - 60
README.md

@@ -108,66 +108,7 @@ For more info, see [GROWI Docs: List of npm Commands](https://docs.growi.org/en/
 Environment Variables
 Environment Variables
 ======================
 ======================
 
 
-* **Required**
-    * MONGO_URI: URI to connect to MongoDB.
-* **Option**
-    * NODE_ENV: `production` OR `development`.
-    * PORT: Server port. default: `3000`.
-    * NO_CDN: If `true`, system doesn't use CDN, all resources will be downloaded from CDN when build client, and served by the GROWI Express server. default: `false`.
-    * ELASTICSEARCH_URI: URI to connect to Elasticearch.
-    * REDIS_URI: URI to connect to Redis (use it as a session store instead of MongoDB).
-    * PASSWORD_SEED: A password seed used by password hash generator.
-    * SECRET_TOKEN: A secret key for verifying the integrity of signed cookies.
-    * SESSION_NAME: The name of the session ID cookie to set in the response by Express. default: `connect.sid`
-    * SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: If `true`, the system uses only the value of the environment variable as the value of the SAML option that can be set via the environment variable.
-    * PUBLISH_OPEN_API: Publish GROWI OpenAPI resources with [ReDoc](https://github.com/Rebilly/ReDoc). Visit `/api-docs`.
-    * FORCE_WIKI_MODE: Forces wiki mode. default: undefined
-        * `public`  : Forces all pages to become public
-        * `private` : Forces all pages to become private
-        * undefined : Publicity will be configured by the admin security page settings
-    * FORMAT_NODE_LOG: If `false`, Output server log as JSON. defautl: `true` (Enabled only when `NODE_ENV=production`)
-* **Option for file uploading**
-    * FILE_UPLOAD: Attached files storage. default: `aws`
-        * `aws` : Amazon Web Service S3 (needs AWS settings on Admin page)
-        * `gcs` : Google Cloud Storage (needs settings with environment variables)
-        * `mongodb` : MongoDB GridFS (Setting-less)
-        * `local` : Server's Local file system (Setting-less)
-        * `none` : Disable file uploading
-    * FILE_UPLOAD_DISABLED: If `true`, file uploading will be disabled. However, the files can be still viewed. Default: `false`
-    * MAX_FILE_SIZE: The maximum file size limit for uploads (bytes). default: `Infinity`
-    * FILE_UPLOAD_TOTAL_LIMIT: Total capacity limit for uploads (bytes). default: `Infinity`
-    * GCS_API_KEY_JSON_PATH: Path of the JSON file that contains [service account key to authenticate to GCP API](https://cloud.google.com/iam/docs/creating-managing-service-account-keys)
-    * GCS_BUCKET: Name of the GCS bucket
-    * GCS_UPLOAD_NAMESPACE: Directory name to create in the bucket
-    * MONGO_GRIDFS_TOTAL_LIMIT: Total capacity limit of MongoDB GridFS (bytes). default: `Infinity`
-        * MONGO_GRIDFS_TOTAL_LIMIT setting takes precedence over FILE_UPLOAD_TOTAL_LIMIT.
-* **Option to integrate with external systems**
-    * HACKMD_URI: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server.
-        * **This server must load the GROWI agent. [Here's how to prepare it](https://docs.growi.org/guide/admin-cookbook/integrate-with-hackmd.html).**
-    * HACKMD_URI_FOR_SERVER: URI to connect to [HackMD(CodiMD)](https://hackmd.io/) server from GROWI Express server. If not set, `HACKMD_URI` will be used.
-    * PLANTUML_URI: URI to connect to [PlantUML](http://plantuml.com/) server.
-    * BLOCKDIAG_URI: URI to connect to [blockdiag](http://http://blockdiag.com/) server.
-* **Option (Overwritable in admin page)**
-    * APP_SITE_URL: Site URL. e.g. `https://example.com`, `https://example.com:8080`
-    * LOCAL_STRATEGY_ENABLED: Enable or disable ID/Pass login
-    * LOCAL_STRATEGY_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: Prioritize env vars than values in DB for some ID/Pass login options
-    * SAML_ENABLED: Enable or disable SAML
-    * SAML_USES_ONLY_ENV_VARS_FOR_SOME_OPTIONS: Prioritize env vars than values in DB for some SAML options
-    * SAML_ENTRY_POINT: IdP entry point
-    * SAML_ISSUER: Issuer string to supply to IdP
-    * SAML_ATTR_MAPPING_ID: Attribute map for id
-    * SAML_ATTR_MAPPING_USERNAME: Attribute map for username
-    * SAML_ATTR_MAPPING_MAIL: Attribute map for email
-    * SAML_ATTR_MAPPING_FIRST_NAME: Attribute map for first name
-    * SAML_ATTR_MAPPING_LAST_NAME:  Attribute map for last name
-    * SAML_CERT: PEM-encoded X.509 signing certificate string to validate the response from IdP
-    * OAUTH_GOOGLE_CLIENT_ID: Google API client id for OAuth login.
-    * OAUTH_GOOGLE_CLIENT_SECRET: Google API client secret for OAuth login.
-    * OAUTH_GITHUB_CLIENT_ID: GitHub API client id for OAuth login.
-    * OAUTH_GITHUB_CLIENT_SECRET: GitHub API client secret for OAuth login.
-    * OAUTH_TWITTER_CONSUMER_KEY: Twitter consumer key(API key) for OAuth login.
-    * OAUTH_TWITTER_CONSUMER_SECRET: Twitter consumer secret(API secret) for OAuth login.
-
+- [GROWI Docs Environment Variables](https://docs.growi.org/en/admin-guide/admin-cookbook/env-vars.html)
 
 
 Documentation
 Documentation
 ==============
 ==============

+ 1 - 1
app.json

@@ -27,7 +27,7 @@
     },
     },
     "ADDITIONAL_PACKAGES": {
     "ADDITIONAL_PACKAGES": {
       "description": "Space-separated list of npm package names to install.",
       "description": "Space-separated list of npm package names to install.",
-      "value": "growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images react-motion",
+      "value": "growi-plugin-lsx growi-plugin-pukiwiki-like-linker growi-plugin-attachment-refs react-images@1.0.0 react-motion",
       "required": false
       "required": false
     }
     }
   },
   },

+ 4 - 7
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "growi",
   "name": "growi",
-  "version": "3.6.2-RC",
+  "version": "3.6.3-RC",
   "description": "Team collaboration software using markdown",
   "description": "Team collaboration software using markdown",
   "tags": [
   "tags": [
     "wiki",
     "wiki",
@@ -35,7 +35,7 @@
     "heroku-postbuild": "sh bin/heroku/install-packages.sh && npm run build:prod",
     "heroku-postbuild": "sh bin/heroku/install-packages.sh && npm run build:prod",
     "lint:js:fix": "eslint \"**/*.{js,jsx}\" --fix",
     "lint:js:fix": "eslint \"**/*.{js,jsx}\" --fix",
     "lint:js": "eslint \"**/*.{js,jsx}\"",
     "lint:js": "eslint \"**/*.{js,jsx}\"",
-    "lint:styles:fix": "prettier-stylelint --quiet --write src/client/styles/scss/**/*.scss",
+    "lint:styles:fix": "stylelint --fix src/client/styles/scss/**/*.scss",
     "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
     "lint:styles": "stylelint src/client/styles/scss/**/*.scss",
     "lint:swagger2openapi": "node node_modules/swagger2openapi/oas-validate tmp/swagger.json",
     "lint:swagger2openapi": "node node_modules/swagger2openapi/oas-validate tmp/swagger.json",
     "lint": "npm-run-all -p lint:js lint:styles lint:swagger2openapi",
     "lint": "npm-run-all -p lint:js lint:styles lint:swagger2openapi",
@@ -101,7 +101,6 @@
     "express-webpack-assets": "^0.1.0",
     "express-webpack-assets": "^0.1.0",
     "graceful-fs": "^4.1.11",
     "graceful-fs": "^4.1.11",
     "growi-commons": "^4.0.8",
     "growi-commons": "^4.0.8",
-    "growi-plugin-attachment-refs": "^1.0.6",
     "helmet": "^3.13.0",
     "helmet": "^3.13.0",
     "i18next": "^19.0.0",
     "i18next": "^19.0.0",
     "i18next-express-middleware": "^1.4.1",
     "i18next-express-middleware": "^1.4.1",
@@ -125,7 +124,7 @@
     "openid-client": "=2.5.0",
     "openid-client": "=2.5.0",
     "passport": "^0.4.0",
     "passport": "^0.4.0",
     "passport-github": "^1.1.0",
     "passport-github": "^1.1.0",
-    "passport-google-auth": "^1.0.2",
+    "passport-google-oauth20": "^2.0.0",
     "passport-http": "^0.3.0",
     "passport-http": "^0.3.0",
     "passport-ldapauth": "^2.0.0",
     "passport-ldapauth": "^2.0.0",
     "passport-local": "^1.0.0",
     "passport-local": "^1.0.0",
@@ -210,7 +209,6 @@
     "penpal": "^4.0.0",
     "penpal": "^4.0.0",
     "plantuml-encoder": "^1.2.5",
     "plantuml-encoder": "^1.2.5",
     "postcss-loader": "^3.0.0",
     "postcss-loader": "^3.0.0",
-    "prettier-stylelint": "^0.4.2",
     "react": "^16.8.3",
     "react": "^16.8.3",
     "react-bootstrap": "^0.32.1",
     "react-bootstrap": "^0.32.1",
     "react-bootstrap-typeahead": "^3.4.2",
     "react-bootstrap-typeahead": "^3.4.2",
@@ -221,8 +219,6 @@
     "react-frame-component": "^4.0.0",
     "react-frame-component": "^4.0.0",
     "react-hotkeys": "^2.0.0",
     "react-hotkeys": "^2.0.0",
     "react-i18next": "^11.1.0",
     "react-i18next": "^11.1.0",
-    "react-images": "1.0.0",
-    "react-motion": "^0.5.2",
     "react-waypoint": "^9.0.0",
     "react-waypoint": "^9.0.0",
     "replacestream": "^4.0.3",
     "replacestream": "^4.0.3",
     "reveal.js": "^3.5.0",
     "reveal.js": "^3.5.0",
@@ -230,6 +226,7 @@
     "simple-load-script": "^1.0.2",
     "simple-load-script": "^1.0.2",
     "socket.io-client": "^2.0.3",
     "socket.io-client": "^2.0.3",
     "style-loader": "^1.0.0",
     "style-loader": "^1.0.0",
+    "stylelint": "^12.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "stylelint-config-recess-order": "^2.0.1",
     "swagger-jsdoc": "^3.4.0",
     "swagger-jsdoc": "^3.4.0",
     "swagger2openapi": "^5.3.1",
     "swagger2openapi": "^5.3.1",

+ 57 - 101
resource/locales/en-US/translation.json

@@ -28,13 +28,11 @@
   "Page Path": "Page Path",
   "Page Path": "Page Path",
   "Category": "Category",
   "Category": "Category",
   "User": "User",
   "User": "User",
-  "status":"Status",
+  "status": "Status",
   "account_id": "Account Id",
   "account_id": "Account Id",
-
   "Update": "Update",
   "Update": "Update",
   "Update Page": "Update Page",
   "Update Page": "Update Page",
   "Warning": "Warning",
   "Warning": "Warning",
-
   "Sign in": "Sign in",
   "Sign in": "Sign in",
   "Sign up is here": "Sign up",
   "Sign up is here": "Sign up",
   "Sign in is here": "Sign in",
   "Sign in is here": "Sign in",
@@ -44,30 +42,23 @@
   "Sign up with this Google Account": "Sign up with this Google Account",
   "Sign up with this Google Account": "Sign up with this Google Account",
   "Example": "Example",
   "Example": "Example",
   "Taro Yamada": "John Doe",
   "Taro Yamada": "John Doe",
-
   "List View": "List",
   "List View": "List",
   "Timeline View": "Timeline",
   "Timeline View": "Timeline",
   "History": "History",
   "History": "History",
   "Presentation Mode": "Presentation",
   "Presentation Mode": "Presentation",
-
   "username": "Username",
   "username": "Username",
   "Created": "Created",
   "Created": "Created",
   "Last updated": "Updated",
   "Last updated": "Updated",
   "Last_Login": "Last Login",
   "Last_Login": "Last Login",
-
   "Share": "Share",
   "Share": "Share",
   "Share Link": "Share Link",
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
   "Markdown Link": "Markdown Link",
-
   "Create/Edit Template": "Create/Edit Template Page",
   "Create/Edit Template": "Create/Edit Template Page",
-
   "Unportalize": "Unportalize",
   "Unportalize": "Unportalize",
-
   "Go to this version": "View this version",
   "Go to this version": "View this version",
   "View diff": "View diff",
   "View diff": "View diff",
   "No diff": "No diff",
   "No diff": "No diff",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
-
   "User ID": "User ID",
   "User ID": "User ID",
   "Home": "Home",
   "Home": "Home",
   "User Settings": "User Settings",
   "User Settings": "User Settings",
@@ -89,19 +80,15 @@
   "Show": "Show",
   "Show": "Show",
   "Hide": "Hide",
   "Hide": "Hide",
   "Disclose E-mail": "Disclose E-mail",
   "Disclose E-mail": "Disclose E-mail",
-
   "page exists": "this page already exists",
   "page exists": "this page already exists",
   "Error occurred": "Error occurred",
   "Error occurred": "Error occurred",
-
   "Create today's": "Create today's ...",
   "Create today's": "Create today's ...",
   "Memo": "memo",
   "Memo": "memo",
   "Input page name": "Input page name",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New Page",
   "New Page": "New Page",
   "Create under": "Create page under below:",
   "Create under": "Create page under below:",
-
   "Table of Contents": "Table of Contents",
   "Table of Contents": "Table of Contents",
-
   "Management Wiki Home": "Management Wiki Home",
   "Management Wiki Home": "Management Wiki Home",
   "App Settings": "App Settings",
   "App Settings": "App Settings",
   "Site URL settings": "Site URL settings",
   "Site URL settings": "Site URL settings",
@@ -129,30 +116,24 @@
   "Add tags for this page": "Add tags for this page",
   "Add tags for this page": "Add tags for this page",
   "Edit tags for this page": "Edit tags for this page",
   "Edit tags for this page": "Edit tags for this page",
   "You have no tag, You can set tags on pages": "You have no tag, You can set tags on pages",
   "You have no tag, You can set tags on pages": "You have no tag, You can set tags on pages",
-
   "Show latest": "Show latest",
   "Show latest": "Show latest",
   "Load latest": "Load latest",
   "Load latest": "Load latest",
   "edited this page": "edited this page.",
   "edited this page": "edited this page.",
-
   "List Drafts": "Drafts",
   "List Drafts": "Drafts",
   "Deleted Pages": "Deleted Pages",
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
   "Sign out": "Logout",
-
   "form_validation": {
   "form_validation": {
     "required": "<code>%s</code> is required"
     "required": "<code>%s</code> is required"
   },
   },
-
   "installer": {
   "installer": {
     "setup": "Setup",
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "create_initial_account": "Create an initial account",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
     "unavaliable_user_id": "This 'User ID' is unavailable."
     "unavaliable_user_id": "This 'User ID' is unavailable."
   },
   },
-
   "breaking_changes": {
   "breaking_changes": {
     "v346_using_basic_auth": "Basic Authentication currently in use will <strong>no longer be available</strong> in the near future. Remove settings from %s"
     "v346_using_basic_auth": "Basic Authentication currently in use will <strong>no longer be available</strong> in the near future. Remove settings from %s"
   },
   },
-
   "page_register": {
   "page_register": {
     "notice": {
     "notice": {
       "restricted": "Admin approval required.",
       "restricted": "Admin approval required.",
@@ -164,7 +145,6 @@
       "user_id": "The URL of pages you create will contain your User ID. Your User ID can consist of letters, numbers, and some symbols."
       "user_id": "The URL of pages you create will contain your User ID. Your User ID can consist of letters, numbers, and some symbols."
     }
     }
   },
   },
-
   "page_me": {
   "page_me": {
     "form_help": {
     "form_help": {
       "profile_image1": "Image upload settings not completed.",
       "profile_image1": "Image upload settings not completed.",
@@ -177,10 +157,8 @@
       "update_token1": "You can update to generate a new API Token.",
       "update_token1": "You can update to generate a new API Token.",
       "update_token2": "You will need to update the API Token in any existing processes."
       "update_token2": "You will need to update the API Token in any existing processes."
     },
     },
-    "form_help": {
-    }
+    "form_help": {}
   },
   },
-
   "Password": "Password",
   "Password": "Password",
   "Password Settings": "Password Settings",
   "Password Settings": "Password Settings",
   "Set new Password": "Set new Password",
   "Set new Password": "Set new Password",
@@ -189,14 +167,11 @@
   "New password": "New password",
   "New password": "New password",
   "Re-enter new password": "Re-enter new password",
   "Re-enter new password": "Re-enter new password",
   "Password is not set": "Password is not set",
   "Password is not set": "Password is not set",
-
   "security_settings": "Security Settings",
   "security_settings": "Security Settings",
-
   "API Settings": "API Settings",
   "API Settings": "API Settings",
   "API Token Settings": "API Token Settings",
   "API Token Settings": "API Token Settings",
   "Current API Token": "Current API Token",
   "Current API Token": "Current API Token",
   "Update API Token": "Update API Token",
   "Update API Token": "Update API Token",
-
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "This tree": "This tree"
       "This tree": "This tree"
@@ -205,7 +180,6 @@
       "This tree": "Only children of this tree"
       "This tree": "Only children of this tree"
     }
     }
   },
   },
-
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
     "Page path": "Page path",
@@ -213,7 +187,6 @@
     "Page path and parmanent link": "Page path and parmanent link",
     "Page path and parmanent link": "Page path and parmanent link",
     "Markdown link": "Markdown link"
     "Markdown link": "Markdown link"
   },
   },
-
   "search_help": {
   "search_help": {
     "title": "Searching Help",
     "title": "Searching Help",
     "and": {
     "and": {
@@ -243,7 +216,6 @@
   "search": {
   "search": {
     "search page bodies": "Hit [Enter] key to full-text search"
     "search page bodies": "Hit [Enter] key to full-text search"
   },
   },
-
   "page_page": {
   "page_page": {
     "notice": {
     "notice": {
       "version": "This is not the current version.",
       "version": "This is not the current version.",
@@ -254,7 +226,6 @@
       "restricted": "Access to this page is restricted"
       "restricted": "Access to this page is restricted"
     }
     }
   },
   },
-
   "page_edit": {
   "page_edit": {
     "Show active line": "Show active line",
     "Show active line": "Show active line",
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
     "overwrite_scopes": "{{operation}} and Overwrite scopes of all descendants",
@@ -262,14 +233,12 @@
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
       "conflict": "Couldn't save the changes you made because someone else was editing this page. Please re-edit the affected section after reloading the page."
     }
     }
   },
   },
-
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "already_exists": "New page is already exists.",
     "already_exists": "New page is already exists.",
     "outdated": "Page is updated someone and now outdated.",
     "outdated": "Page is updated someone and now outdated.",
     "user_not_admin": "Only admin user can delete completely"
     "user_not_admin": "Only admin user can delete completely"
   },
   },
-
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
       "Move/Rename page": "Move/Rename page",
       "Move/Rename page": "Move/Rename page",
@@ -285,10 +254,8 @@
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
     }
     }
   },
   },
-
   "Put Back": "Put Back",
   "Put Back": "Put Back",
   "Delete Completely": "Delete Completely",
   "Delete Completely": "Delete Completely",
-
   "modal_delete": {
   "modal_delete": {
     "delete_page": "Delete Page",
     "delete_page": "Delete Page",
     "deleting_page": "Deleting Page",
     "deleting_page": "Deleting Page",
@@ -298,7 +265,6 @@
     "recursively": "Delete children of under <code>%s</code> recursively.",
     "recursively": "Delete children of under <code>%s</code> recursively.",
     "completely": "Delete completely instead of putting it into trash."
     "completely": "Delete completely instead of putting it into trash."
   },
   },
-
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {
       "Duplicate page": "Duplicate page",
       "Duplicate page": "Duplicate page",
@@ -306,7 +272,6 @@
       "Current page name": "Current page name"
       "Current page name": "Current page name"
     }
     }
   },
   },
-
   "modal_putback": {
   "modal_putback": {
     "label": {
     "label": {
       "Put Back Page": "Put Back Page",
       "Put Back Page": "Put Back Page",
@@ -316,7 +281,6 @@
       "recursively": "Put Back children of under <code>%s</code> recursively"
       "recursively": "Put Back children of under <code>%s</code> recursively"
     }
     }
   },
   },
-
   "modal_shortcuts": {
   "modal_shortcuts": {
     "global": {
     "global": {
       "title": "Global shortcuts",
       "title": "Global shortcuts",
@@ -339,7 +303,6 @@
       "Post": "Post"
       "Post": "Post"
     }
     }
   },
   },
-
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit Template Page",
       "Create/Edit Template Page": "Create/Edit Template Page",
@@ -358,7 +321,6 @@
       "desc": "Applies to all decendant pages"
       "desc": "Applies to all decendant pages"
     }
     }
   },
   },
-
   "sandbox": {
   "sandbox": {
     "header": "Header",
     "header": "Header",
     "header_x": "Header {{index}}",
     "header_x": "Header {{index}}",
@@ -390,7 +352,6 @@
     "insert_image": "inserts an image",
     "insert_image": "inserts an image",
     "open_sandbox": "Open Sandbox"
     "open_sandbox": "Open Sandbox"
   },
   },
-
   "admin_top": {
   "admin_top": {
     "Management Wiki": "Management Wiki",
     "Management Wiki": "Management Wiki",
     "System Information": "System Information",
     "System Information": "System Information",
@@ -401,7 +362,6 @@
     "Specified version": "Specified version",
     "Specified version": "Specified version",
     "Installed version": "Installed version"
     "Installed version": "Installed version"
   },
   },
-
   "app_setting": {
   "app_setting": {
     "Site Name": "Site name",
     "Site Name": "Site name",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
     "sitename_change": "You can change Site Name which is used for header and HTML title.",
@@ -411,7 +371,7 @@
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "siteurl_help": "Site full URL beginning from <code>http://</code> or <code>https://</code>.",
     "Confidential name": "Confidential name",
     "Confidential name": "Confidential name",
     "Default Language for new users": "Default Language for new users",
     "Default Language for new users": "Default Language for new users",
-    "ex): internal use only":"ex): internal use only",
+    "ex): internal use only": "ex): internal use only",
     "File Uploading": "File Uploading",
     "File Uploading": "File Uploading",
     "enable_files_except_image": "Enable file upload other than image files.",
     "enable_files_except_image": "Enable file upload other than image files.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
     "attach_enable": "You can attach files other than image files if you enable this option.",
@@ -421,7 +381,7 @@
     "SMTP_but_AWS": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
     "SMTP_but_AWS": "If you do not have SMTP settings but AWS settings,  e-mails will be sent by SES.",
     "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
     "neihter_of": "If you do not of neither of these, e-mails will not be sent.",
     "From e-mail address": "From e-mail address",
     "From e-mail address": "From e-mail address",
-    "SMTP settings": "SMTP settings"  ,
+    "SMTP settings": "SMTP settings",
     "Host": "Host",
     "Host": "Host",
     "Port": "Port",
     "Port": "Port",
     "User": "User",
     "User": "User",
@@ -440,19 +400,18 @@
     "Disable": "Disable",
     "Disable": "Disable",
     "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."
   },
   },
-
   "security_setting": {
   "security_setting": {
-		"Security settings": "Security settings",
+    "Security settings": "Security settings",
     "Guest Users Access": "Guest Users Access",
     "Guest Users Access": "Guest Users Access",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Register limitation": "Register limitation",
     "Register limitation": "Register limitation",
     "Register limitation desc": "Restricts ways to register new user.",
     "Register limitation desc": "Restricts ways to register new user.",
-		"The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
-		"users_without_account": "Users without account is not accessible",
+    "The whitelist of registration permission E-mail address": "The whitelist of registration permission E-mail address",
+    "users_without_account": "Users without account is not accessible",
     "example": "Example",
     "example": "Example",
     "restrict_emails": "You can restrict registerable e-mail address.",
     "restrict_emails": "You can restrict registerable e-mail address.",
-		"for_instance": " For instance, if you use growi within a company, you can write ",
-		"only_those": " Only those whose e-mail address including the company address can register.",
+    "for_instance": " For instance, if you use growi within a company, you can write ",
+    "only_those": " Only those whose e-mail address including the company address can register.",
     "insert_single": "Please insert single e-mail address per line.",
     "insert_single": "Please insert single e-mail address per line.",
     "page_listing_1": "Page listing/searching<br>restricted by 'Just Me'",
     "page_listing_1": "Page listing/searching<br>restricted by 'Just Me'",
     "page_listing_1_desc": "Show pages that are restricted by 'Just Me' option when listing/searching",
     "page_listing_1_desc": "Show pages that are restricted by 'Just Me' option when listing/searching",
@@ -463,11 +422,10 @@
     "admin_only": "Admin Only",
     "admin_only": "Admin Only",
     "admin_and_author": "Admin and Author",
     "admin_and_author": "Admin and Author",
     "anyone": "Anyone",
     "anyone": "Anyone",
-
-		"Authentication mechanism settings": "Authentication Mechanism Settings",
+    "Authentication mechanism settings": "Authentication Mechanism Settings",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
     "alert_siteUrl_is_not_set": "'Site URL' is NOT set. Set it from the {{link}}",
-    "xss_prevent_setting":"Prevent XSS(Cross Site Scripting)",
-    "xss_prevent_setting_link":"Go to Markdown settings",
+    "xss_prevent_setting": "Prevent XSS(Cross Site Scripting)",
+    "xss_prevent_setting_link": "Go to Markdown settings",
     "callback_URL": "Callback URL",
     "callback_URL": "Callback URL",
     "providerName": "Provider Name",
     "providerName": "Provider Name",
     "issuerHost": "Issuer Host",
     "issuerHost": "Issuer Host",
@@ -534,7 +492,7 @@
     },
     },
     "SAML": {
     "SAML": {
       "name": "SAML",
       "name": "SAML",
-      "enable_saml":"enable SAML",
+      "enable_saml": "enable SAML",
       "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
       "id_detail": "Specification of the name of attribute which can identify the user in SAML Identity Provider",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
       "username_detail": "Specification of mappings for <code>username</code> when creating new users",
       "mapping_detail": "Specification of mappings for {{target}} when creating new users",
       "mapping_detail": "Specification of mappings for {{target}} when creating new users",
@@ -544,7 +502,7 @@
       "updated_saml": "Succeeded to update SAML setting"
       "updated_saml": "Succeeded to update SAML setting"
     },
     },
     "Basic": {
     "Basic": {
-      "enable_basic":"enable Basic",
+      "enable_basic": "enable Basic",
       "name": "Basic Authentication",
       "name": "Basic Authentication",
       "desc_1": "Login with <code>username</code> in Authorization header.",
       "desc_1": "Login with <code>username</code> in Authorization header.",
       "desc_2": "User will be automatically generated if not exist.",
       "desc_2": "User will be automatically generated if not exist.",
@@ -555,7 +513,7 @@
       "register": "Register for %s",
       "register": "Register for %s",
       "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
       "change_redirect_url": "Enter <code>%s</code> <br>(where <code>%s</code> is your host name) for \"Authorized redirect URIs\".",
       "Google": {
       "Google": {
-        "enable_google":"enable Google OAuth",
+        "enable_google": "enable Google OAuth",
         "name": "Google OAuth",
         "name": "Google OAuth",
         "register_1": "Access {{link}}",
         "register_1": "Access {{link}}",
         "register_2": "Create Project if no projects exist",
         "register_2": "Create Project if no projects exist",
@@ -578,7 +536,7 @@
         "updated_twitter": "Succeeded to update Twitter OAuth setting"
         "updated_twitter": "Succeeded to update Twitter OAuth setting"
       },
       },
       "GitHub": {
       "GitHub": {
-        "enable_github":"enable GitHub OAuth",
+        "enable_github": "enable GitHub OAuth",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
         "register_1": "Access {{link}}",
         "register_1": "Access {{link}}",
         "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
         "register_2": "Register your OAuth App with \"Authorization callback URL\" as <code>{{url}}</code>",
@@ -613,8 +571,7 @@
       "attrMapFirstName": "First Name",
       "attrMapFirstName": "First Name",
       "attrMapLastName": "Last Name"
       "attrMapLastName": "Last Name"
     }
     }
-	},
-
+  },
   "markdown_setting": {
   "markdown_setting": {
     "line_break_setting": "Line Break Setting",
     "line_break_setting": "Line Break Setting",
     "line_break_setting_desc": "You can change line break settings.",
     "line_break_setting_desc": "You can change line break settings.",
@@ -640,14 +597,13 @@
     "Ignore all tags desc": "Stripe all HTML tags and attributes",
     "Ignore all tags desc": "Stripe all HTML tags and attributes",
     "Recommended setting": "Recommended Setting",
     "Recommended setting": "Recommended Setting",
     "Custom Whitelist": "Custom Whitelist",
     "Custom Whitelist": "Custom Whitelist",
-    "Tag names":"Tag names",
-    "Tag attributes":"Tag attributes",
+    "Tag names": "Tag names",
+    "Tag attributes": "Tag attributes",
     "import_recommended": "Import recommended %s",
     "import_recommended": "Import recommended %s",
     "updated_lineBreak": "Succeeded to update line braek setting",
     "updated_lineBreak": "Succeeded to update line braek setting",
     "updated_presentation": "Succeeded to update presentation setting",
     "updated_presentation": "Succeeded to update presentation setting",
     "updated_xss": "Succeeded to update XSS setting"
     "updated_xss": "Succeeded to update XSS setting"
   },
   },
-
   "notification_setting": {
   "notification_setting": {
     "notification_list": "List of Notification Settings",
     "notification_list": "List of Notification Settings",
     "add_notification": "Add New",
     "add_notification": "Add New",
@@ -667,9 +623,8 @@
       "ifttt_link": "Create a new IFTTT applet with Email trigger"
       "ifttt_link": "Create a new IFTTT applet with Email trigger"
     }
     }
   },
   },
-
   "customize_page": {
   "customize_page": {
-    "recommended":"Recommended",
+    "recommended": "Recommended",
     "Behavior": "Behavior",
     "Behavior": "Behavior",
     "Layout": "Layout",
     "Layout": "Layout",
     "Function": "Function",
     "Function": "Function",
@@ -706,32 +661,31 @@
     "update_customHeader_success": "Succeeded to update customize html header",
     "update_customHeader_success": "Succeeded to update customize html header",
     "update_customCss_success": "Succeeded to update customize css",
     "update_customCss_success": "Succeeded to update customize css",
     "update_script_success": "Succeeded to update custom script",
     "update_script_success": "Succeeded to update custom script",
-    "layout_description":{
-      "growi_title":"Simple and Clear",
-      "growi_text1":"Full screen layout and thin margins/paddings",
-      "growi_text2":"Show and post comments at the bottom of the page",
-      "growi_text3":"Affix Table-of-contents",
-      "kibela_title":"Easy Viewing Structure",
-      "kibela_text1":"Center aligned contents",
-      "kibela_text2":"Show and post comments at the bottom of the page",
-      "kibela_text3":"Affix Table-of-contents",
-      "crowi_title":"Separated Functions",
-      "crowi_text1":"Collapsible Sidebar",
-      "crowi_text2":"Show and post comments in Sidebar",
-      "crowi_text3":"Collapsible Table-of-contents"
+    "layout_description": {
+      "growi_title": "Simple and Clear",
+      "growi_text1": "Full screen layout and thin margins/paddings",
+      "growi_text2": "Show and post comments at the bottom of the page",
+      "growi_text3": "Affix Table-of-contents",
+      "kibela_title": "Easy Viewing Structure",
+      "kibela_text1": "Center aligned contents",
+      "kibela_text2": "Show and post comments at the bottom of the page",
+      "kibela_text3": "Affix Table-of-contents",
+      "crowi_title": "Separated Functions",
+      "crowi_text1": "Collapsible Sidebar",
+      "crowi_text2": "Show and post comments in Sidebar",
+      "crowi_text3": "Collapsible Table-of-contents"
     },
     },
-    "behavior_description":{
-      "growi_text1":"Both of <code>/page</code> and <code>/page/</code> shows the same page。",
-      "growi_text2":"<code>/nonexistent_page</code> shows editing form",
-      "growi_text3":"All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b>",
-      "crowi_text1":"<code>/page</code> shows the page",
-      "crowi_text2":"<code>/page/</code> shows the list of sub pages",
-      "crowi_text3":"If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown",
-      "crowi_text4":"<code>/nonexistent_page</code> shows editing form<",
-      "crowi_text5":"<code>/nonexistent_page/</code> the list of sub pages"
+    "behavior_description": {
+      "growi_text1": "Both of <code>/page</code> and <code>/page/</code> shows the same page。",
+      "growi_text2": "<code>/nonexistent_page</code> shows editing form",
+      "growi_text3": "All pages shows the list of sub pages <b>if using GROWI Enhanced Layout</b>",
+      "crowi_text1": "<code>/page</code> shows the page",
+      "crowi_text2": "<code>/page/</code> shows the list of sub pages",
+      "crowi_text3": "If portal is applied to <code>/page/</code> , the portal and the list of sub pages are shown",
+      "crowi_text4": "<code>/nonexistent_page</code> shows editing form<",
+      "crowi_text5": "<code>/nonexistent_page/</code> the list of sub pages"
     }
     }
   },
   },
-
   "user_management": {
   "user_management": {
     "target_user": "Target User",
     "target_user": "Target User",
     "new_password": "New Password",
     "new_password": "New Password",
@@ -760,10 +714,10 @@
     "reset_password": "Reset Password",
     "reset_password": "Reset Password",
     "related_username": "Related user's ",
     "related_username": "Related user's ",
     "accept": "Accept",
     "accept": "Accept",
-    "deactivate_account":"Deactivate Account",
-    "your_own":"You cannot deactivate your own account",
-    "administrator_menu":"Administrator Menu",
-    "cannot_remove":"You cannot remove yourself from administrator",
+    "deactivate_account": "Deactivate Account",
+    "your_own": "You cannot deactivate your own account",
+    "administrator_menu": "Administrator Menu",
+    "cannot_remove": "You cannot remove yourself from administrator",
     "cannot_invite_maximum_users": "Can not invite more than the maximum number of users.",
     "cannot_invite_maximum_users": "Can not invite more than the maximum number of users.",
     "current_users": "Current users:",
     "current_users": "Current users:",
     "valid_email": "Valid email address is required",
     "valid_email": "Valid email address is required",
@@ -775,8 +729,12 @@
     "remove_user_success": "Succeeded to removing {{username}} ",
     "remove_user_success": "Succeeded to removing {{username}} ",
     "remove_external_user_success": "Succeeded to remove {{accountId}} "
     "remove_external_user_success": "Succeeded to remove {{accountId}} "
   },
   },
-
   "user_group_management": {
   "user_group_management": {
+    "search_option": "Search Option",
+    "enable_option": "Enable {{option}}",
+    "forward_match": "forword match",
+    "partial_match": "partial match",
+    "backward_match": "backward match",
     "group_list": "Group List",
     "group_list": "Group List",
     "back_to_list": "Go Back to Group List",
     "back_to_list": "Go Back to Group List",
     "basic_info": "Basic Info",
     "basic_info": "Basic Info",
@@ -798,7 +756,6 @@
     "no_pages": "There are no pages the group has view permission",
     "no_pages": "There are no pages the group has view permission",
     "remove_from_group": "Remove this user"
     "remove_from_group": "Remove this user"
   },
   },
-
   "importer_management": {
   "importer_management": {
     "beta_warning": "This function is Beta.",
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
     "import_from": "Import from {{from}}",
@@ -857,13 +814,12 @@
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "Directory_hierarchy_tag": "Directory Hierarchy Tag"
     "Directory_hierarchy_tag": "Directory Hierarchy Tag"
   },
   },
-
-  "full_text_search_management":{
-    "elasticsearch_management":"Elasticsearch Management",
-    "build_button":"Rebuild Index",
-    "rebuild_description_1":"Force rebuild index.",
-    "rebuild_description_2":"Click 'Build Now' to delete and create mapping file and add all pages.",
-    "rebuild_description_3":"This may take a while."
+  "full_text_search_management": {
+    "elasticsearch_management": "Elasticsearch Management",
+    "build_button": "Rebuild Index",
+    "rebuild_description_1": "Force rebuild index.",
+    "rebuild_description_2": "Click 'Build Now' to delete and create mapping file and add all pages.",
+    "rebuild_description_3": "This may take a while."
   },
   },
   "export_management": {
   "export_management": {
     "exporting_collection_list": "Exporting Collection List",
     "exporting_collection_list": "Exporting Collection List",

+ 49 - 93
resource/locales/ja/translation.json

@@ -30,11 +30,9 @@
   "User": "ユーザー",
   "User": "ユーザー",
   "status": "ステータス",
   "status": "ステータス",
   "account_id": "アカウントID",
   "account_id": "アカウントID",
-
   "Update": "更新",
   "Update": "更新",
   "Update Page": "ページを更新",
   "Update Page": "ページを更新",
   "Warning": "注意",
   "Warning": "注意",
-
   "Sign in": "ログイン",
   "Sign in": "ログイン",
   "Sign up is here": "新規登録はこちら",
   "Sign up is here": "新規登録はこちら",
   "Sign in is here": "ログインはこちら",
   "Sign in is here": "ログインはこちら",
@@ -44,30 +42,23 @@
   "Sign up with this Google Account": "この Google アカウントで登録します",
   "Sign up with this Google Account": "この Google アカウントで登録します",
   "Example": "例",
   "Example": "例",
   "Taro Yamada": "山田 太郎",
   "Taro Yamada": "山田 太郎",
-
   "List View": "リスト表示",
   "List View": "リスト表示",
   "Timeline View": "タイムライン表示",
   "Timeline View": "タイムライン表示",
   "History": "更新履歴",
   "History": "更新履歴",
   "Presentation Mode": "プレゼンテーション",
   "Presentation Mode": "プレゼンテーション",
-
   "username": "ユーザー名",
   "username": "ユーザー名",
   "Created": "作成日",
   "Created": "作成日",
   "Last updated": "最終更新",
   "Last updated": "最終更新",
   "Last_Login": "最終ログイン",
   "Last_Login": "最終ログイン",
-
   "Share": "共有",
   "Share": "共有",
   "Share Link": "共有用リンク",
   "Share Link": "共有用リンク",
   "Markdown Link": "Markdown形式のリンク",
   "Markdown Link": "Markdown形式のリンク",
-
   "Create/Edit Template": "テンプレートページの作成/編集",
   "Create/Edit Template": "テンプレートページの作成/編集",
-
   "Unportalize": "ポータル解除",
   "Unportalize": "ポータル解除",
-
   "Go to this version": "このバージョンを見る",
   "Go to this version": "このバージョンを見る",
   "View diff": "差分を表示",
   "View diff": "差分を表示",
   "No diff": "差分なし",
   "No diff": "差分なし",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
   "Shrink versions that have no diffs": "差分のないバージョンをコンパクトに表示する",
-
   "User ID": "ユーザーID",
   "User ID": "ユーザーID",
   "Home": "ホーム",
   "Home": "ホーム",
   "User Settings": "ユーザー設定",
   "User Settings": "ユーザー設定",
@@ -89,19 +80,15 @@
   "Show": "公開",
   "Show": "公開",
   "Hide": "非公開",
   "Hide": "非公開",
   "Disclose E-mail": "メールアドレスの公開",
   "Disclose E-mail": "メールアドレスの公開",
-
   "page exists": "このページはすでに存在しています",
   "page exists": "このページはすでに存在しています",
-  "Error occurred":"エラーが発生しました",
-
+  "Error occurred": "エラーが発生しました",
   "Create today's": "今日の◯◯を作成",
   "Create today's": "今日の◯◯を作成",
   "Memo": "メモ",
   "Memo": "メモ",
   "Input page name": "ページ名を入力",
   "Input page name": "ページ名を入力",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "Input page name (optional)": "ページ名を入力(空欄OK)",
   "New Page": "新規ページ",
   "New Page": "新規ページ",
   "Create under": "ページを以下に作成",
   "Create under": "ページを以下に作成",
-
   "Table of Contents": "目次",
   "Table of Contents": "目次",
-
   "Management Wiki Home": "Wiki管理トップ",
   "Management Wiki Home": "Wiki管理トップ",
   "App Settings": "アプリ設定",
   "App Settings": "アプリ設定",
   "Site URL settings": "サイトURL設定",
   "Site URL settings": "サイトURL設定",
@@ -128,34 +115,28 @@
   "Add tags for this page": "タグを付ける",
   "Add tags for this page": "タグを付ける",
   "Edit tags for this page": "タグを編集する",
   "Edit tags for this page": "タグを編集する",
   "You have no tag, You can set tags on pages": "使用中のタグがありません",
   "You have no tag, You can set tags on pages": "使用中のタグがありません",
-
   "Show latest": "最新のページを表示",
   "Show latest": "最新のページを表示",
   "Load latest": "最新版を読み込む",
   "Load latest": "最新版を読み込む",
   "edited this page": "さんがこのページを編集しました。",
   "edited this page": "さんがこのページを編集しました。",
-
   "List Drafts": "下書き一覧",
   "List Drafts": "下書き一覧",
   "Deleted Pages": "削除済みページ",
   "Deleted Pages": "削除済みページ",
   "Sign out": "ログアウト",
   "Sign out": "ログアウト",
-
   "form_validation": {
   "form_validation": {
     "required": "<code>%s</code> に値を入力してください"
     "required": "<code>%s</code> に値を入力してください"
   },
   },
-
   "installer": {
   "installer": {
     "setup": "セットアップ",
     "setup": "セットアップ",
     "create_initial_account": "最初のアカウントの作成",
     "create_initial_account": "最初のアカウントの作成",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
     "initial_account_will_be_administrator_automatically": "初めに作成するアカウントは、自動的に管理者権限が付与されます",
     "unavaliable_user_id": "このユーザーIDは利用できません。"
     "unavaliable_user_id": "このユーザーIDは利用できません。"
   },
   },
-
   "breaking_changes": {
   "breaking_changes": {
     "v346_using_basic_auth": "現在利用中の Basic 認証機能は、近い将来<strong>廃止されます</strong>。%s から設定を削除してください。"
     "v346_using_basic_auth": "現在利用中の Basic 認証機能は、近い将来<strong>廃止されます</strong>。%s から設定を削除してください。"
   },
   },
-
   "page_register": {
   "page_register": {
     "notice": {
     "notice": {
-       "restricted": "この Wiki への新規登録は制限されています。",
-       "restricted_defail": "利用を開始するには、新規登録後、管理者による承認が必要です。"
+      "restricted": "この Wiki への新規登録は制限されています。",
+      "restricted_defail": "利用を開始するには、新規登録後、管理者による承認が必要です。"
     },
     },
     "form_help": {
     "form_help": {
       "email": "この Wiki では以下のメールアドレスのみ登録可能です。",
       "email": "この Wiki では以下のメールアドレスのみ登録可能です。",
@@ -163,7 +144,6 @@
       "user_id": "ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。"
       "user_id": "ユーザーIDは、ユーザーページのURLなどに利用されます。半角英数字と一部の記号のみ利用できます。"
     }
     }
   },
   },
-
   "page_me": {
   "page_me": {
     "form_help": {
     "form_help": {
       "profile_image1": "画像をアップロードをするための設定がされていません。",
       "profile_image1": "画像をアップロードをするための設定がされていません。",
@@ -176,10 +156,8 @@
       "update_token1": "API Token を更新すると、自動的に新しい Token が生成されます。",
       "update_token1": "API Token を更新すると、自動的に新しい Token が生成されます。",
       "update_token2": "現在の Token を利用している処理は動かなくなります。"
       "update_token2": "現在の Token を利用している処理は動かなくなります。"
     },
     },
-    "form_help": {
-    }
+    "form_help": {}
   },
   },
-
   "Password": "パスワード",
   "Password": "パスワード",
   "Password Settings": "パスワード設定",
   "Password Settings": "パスワード設定",
   "Set new Password": "パスワードを新規に設定",
   "Set new Password": "パスワードを新規に設定",
@@ -188,14 +166,11 @@
   "New password": "新しいパスワード",
   "New password": "新しいパスワード",
   "Re-enter new password": "(確認用)",
   "Re-enter new password": "(確認用)",
   "Password is not set": "パスワードが設定されていません",
   "Password is not set": "パスワードが設定されていません",
-
   "security_settings": "セキュリティ設定",
   "security_settings": "セキュリティ設定",
-
   "API Settings": "API設定",
   "API Settings": "API設定",
   "API Token Settings": "API Token設定",
   "API Token Settings": "API Token設定",
   "Current API Token": "現在のAPI Token",
   "Current API Token": "現在のAPI Token",
   "Update API Token": "API Tokenを更新",
   "Update API Token": "API Tokenを更新",
-
   "header_search_box": {
   "header_search_box": {
     "label": {
     "label": {
       "This tree": "この階層"
       "This tree": "この階層"
@@ -204,7 +179,6 @@
       "This tree": "この階層下の子ページのみ"
       "This tree": "この階層下の子ページのみ"
     }
     }
   },
   },
-
   "copy_to_clipboard": {
   "copy_to_clipboard": {
     "Copy to clipboard": "クリップボードにコピー",
     "Copy to clipboard": "クリップボードにコピー",
     "Page path": "ページ名",
     "Page path": "ページ名",
@@ -212,7 +186,6 @@
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Page path and parmanent link": "ページ名とパーマリンク",
     "Markdown link": "マークダウン形式のリンク"
     "Markdown link": "マークダウン形式のリンク"
   },
   },
-
   "search_help": {
   "search_help": {
     "title": "検索のヘルプ",
     "title": "検索のヘルプ",
     "and": {
     "and": {
@@ -242,7 +215,6 @@
   "search": {
   "search": {
     "search page bodies": "[Enter] キー押下で全文検索"
     "search page bodies": "[Enter] キー押下で全文検索"
   },
   },
-
   "page_page": {
   "page_page": {
     "notice": {
     "notice": {
       "version": "これは現在の版ではありません。",
       "version": "これは現在の版ではありません。",
@@ -253,7 +225,6 @@
       "restricted": "このページの閲覧は制限されています"
       "restricted": "このページの閲覧は制限されています"
     }
     }
   },
   },
-
   "page_edit": {
   "page_edit": {
     "Show active line": "アクティブ行をハイライト",
     "Show active line": "アクティブ行をハイライト",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
     "overwrite_scopes": "{{operation}}と同時に全ての配下ページのスコープを上書き",
@@ -261,14 +232,12 @@
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
       "conflict": "すでに他の人がこのページを編集していたため保存できませんでした。ページを再読み込み後、自分の編集箇所のみ再度編集してください。"
     }
     }
   },
   },
-
   "page_api_error": {
   "page_api_error": {
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "notfound_or_forbidden": "元のページが見つからないか、アクセス権がありません。",
     "already_exists": "新しいページが既に存在しています。",
     "already_exists": "新しいページが既に存在しています。",
     "outdated": "ページが他のユーザーによって更新されました。",
     "outdated": "ページが他のユーザーによって更新されました。",
     "user_not_admin": "権限のあるユーザーのみが完全削除できます"
     "user_not_admin": "権限のあるユーザーのみが完全削除できます"
   },
   },
-
   "modal_rename": {
   "modal_rename": {
     "label": {
     "label": {
       "Move/Rename page": "ページを移動/名前変更する",
       "Move/Rename page": "ページを移動/名前変更する",
@@ -284,10 +253,8 @@
       "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
       "recursive": "<code>%s</code> 配下のページも移動/名前変更します"
     }
     }
   },
   },
-
   "Put Back": "元に戻す",
   "Put Back": "元に戻す",
   "Delete Completely": "完全削除",
   "Delete Completely": "完全削除",
-
   "modal_delete": {
   "modal_delete": {
     "delete_page": "ページを削除する",
     "delete_page": "ページを削除する",
     "deleting_page": "ページパス",
     "deleting_page": "ページパス",
@@ -297,7 +264,6 @@
     "recursively": "<code>%s</code> 配下のページも削除します",
     "recursively": "<code>%s</code> 配下のページも削除します",
     "completely": "ゴミ箱を経由せず、完全に削除します"
     "completely": "ゴミ箱を経由せず、完全に削除します"
   },
   },
-
   "modal_duplicate": {
   "modal_duplicate": {
     "label": {
     "label": {
       "Duplicate page": "ページを複製する",
       "Duplicate page": "ページを複製する",
@@ -305,7 +271,6 @@
       "Current page name": "現在のページ名"
       "Current page name": "現在のページ名"
     }
     }
   },
   },
-
   "modal_putback": {
   "modal_putback": {
     "label": {
     "label": {
       "Put Back Page": "ページを元に戻す",
       "Put Back Page": "ページを元に戻す",
@@ -315,7 +280,6 @@
       "recursively": "<code>%s</code> 配下のページも元に戻します"
       "recursively": "<code>%s</code> 配下のページも元に戻します"
     }
     }
   },
   },
-
   "modal_shortcuts": {
   "modal_shortcuts": {
     "global": {
     "global": {
       "title": "グローバルショートカット",
       "title": "グローバルショートカット",
@@ -338,7 +302,6 @@
       "Post": "投稿"
       "Post": "投稿"
     }
     }
   },
   },
-
   "template": {
   "template": {
     "modal_label": {
     "modal_label": {
       "Create/Edit Template Page": "テンプレートページの作成/編集",
       "Create/Edit Template Page": "テンプレートページの作成/編集",
@@ -357,7 +320,6 @@
       "desc": "テンプレートページが存在する下位層のすべてのページに適用されます"
       "desc": "テンプレートページが存在する下位層のすべてのページに適用されます"
     }
     }
   },
   },
-
   "sandbox": {
   "sandbox": {
     "header": "見出し",
     "header": "見出し",
     "header_x": "見出し {{index}}",
     "header_x": "見出し {{index}}",
@@ -389,7 +351,6 @@
     "insert_image": "で画像を挿入できます",
     "insert_image": "で画像を挿入できます",
     "open_sandbox": "Sandbox を開く"
     "open_sandbox": "Sandbox を開く"
   },
   },
-
   "admin_top": {
   "admin_top": {
     "Management Wiki": "Wiki管理",
     "Management Wiki": "Wiki管理",
     "System Information": "システム情報",
     "System Information": "システム情報",
@@ -400,7 +361,6 @@
     "Specified version": "指定バージョン",
     "Specified version": "指定バージョン",
     "Installed version": "インストールされているバージョン"
     "Installed version": "インストールされているバージョン"
   },
   },
-
   "app_setting": {
   "app_setting": {
     "Site Name": "サイト名",
     "Site Name": "サイト名",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
     "sitename_change": "ヘッダーや HTML タイトルに使用されるサイト名を変更できます。",
@@ -420,7 +380,7 @@
     "SMTP_but_AWS": "SMTP設定がなく、AWSの設定がある場合、SESでの送信を試みます。",
     "SMTP_but_AWS": "SMTP設定がなく、AWSの設定がある場合、SESでの送信を試みます。",
     "neihter_of": "どちらの設定もない場合、メールは送信されません。",
     "neihter_of": "どちらの設定もない場合、メールは送信されません。",
     "From e-mail address": "Fromアドレス",
     "From e-mail address": "Fromアドレス",
-    "SMTP settings": "SMTP設定"   ,
+    "SMTP settings": "SMTP設定",
     "Host": "ホスト",
     "Host": "ホスト",
     "Port": "ポート",
     "Port": "ポート",
     "User": "ユーザー",
     "User": "ユーザー",
@@ -438,8 +398,7 @@
     "Enable": "有効",
     "Enable": "有効",
     "Disable": "無効",
     "Disable": "無効",
     "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します"
     "Use env var if empty": "データベース側の値が空の場合、環境変数 <code>{{env}}</code> の値を利用します"
-   },
-
+  },
   "security_setting": {
   "security_setting": {
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Guest Users Access": "ゲストユーザーのアクセス",
     "Fixed by env var": "環境変数 <code>%s=%s</code> により固定されています。",
     "Fixed by env var": "環境変数 <code>%s=%s</code> により固定されています。",
@@ -449,9 +408,9 @@
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "users_without_account": "アカウントを持たないユーザーはアクセス不可",
     "example": "例",
     "example": "例",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
     "restrict_emails": "登録可能なメールアドレスを制限することができます。",
-    "for_instance":"例えば、",
-    "only_those":"と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
-    "insert_single":"1行に1メールアドレス入力してください。",
+    "for_instance": "例えば、",
+    "only_those": "と記載することで、そのドメインのメールアドレスを持っている人のみ登録可能になります。",
+    "insert_single": "1行に1メールアドレス入力してください。",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1": "ページのリスト表示と検索<br>'自分のみ'に閲覧制限しているページ",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_1_desc": "ページのリスト表示や検索結果において、'自分のみ'に閲覧制限をしているページをアクセス権のないユーザーにも表示します。",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
     "page_listing_2": "ページのリスト表示と検索<br>特定グループに閲覧制限しているページ",
@@ -461,11 +420,10 @@
     "admin_only": "管理者のみ可能",
     "admin_only": "管理者のみ可能",
     "admin_and_author": "管理者とページ作者が可能",
     "admin_and_author": "管理者とページ作者が可能",
     "anyone": "誰でも可能",
     "anyone": "誰でも可能",
-
-    "Authentication mechanism settings":"認証機構設定",
+    "Authentication mechanism settings": "認証機構設定",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
     "alert_siteUrl_is_not_set": "'サイトURL' が設定されていません。{{link}} から設定してください。",
-    "xss_prevent_setting":"XSS(Cross Site Scripting)対策設定",
-    "xss_prevent_setting_link":"マークダウン設定ページに移動",
+    "xss_prevent_setting": "XSS(Cross Site Scripting)対策設定",
+    "xss_prevent_setting_link": "マークダウン設定ページに移動",
     "callback_URL": "コールバックURL",
     "callback_URL": "コールバックURL",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "desc_of_callback_URL": "{{AuthName}} プロバイダ側の設定で利用してください。",
     "clientID": "クライアントID",
     "clientID": "クライアントID",
@@ -539,7 +497,7 @@
       "updated_saml": "SAML設定 を更新しました"
       "updated_saml": "SAML設定 を更新しました"
     },
     },
     "Basic": {
     "Basic": {
-      "enable_basic":"Basic を有効にする",
+      "enable_basic": "Basic を有効にする",
       "name": "Basic 認証",
       "name": "Basic 認証",
       "desc_1": "Authorization ヘッダに格納されている <code>username</code> でログインします。",
       "desc_1": "Authorization ヘッダに格納されている <code>username</code> でログインします。",
       "desc_2": "ユーザーが存在しなかった場合は自動生成します。",
       "desc_2": "ユーザーが存在しなかった場合は自動生成します。",
@@ -550,7 +508,7 @@
       "register": "%sに登録",
       "register": "%sに登録",
       "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力",
       "change_redirect_url": "承認済みのリダイレクトURLに、 <code>%s</code> を入力",
       "Google": {
       "Google": {
-        "enable_google":"Google OAuth を有効にする",
+        "enable_google": "Google OAuth を有効にする",
         "name": "Google OAuth",
         "name": "Google OAuth",
         "register_1": "{{link}}へアクセス",
         "register_1": "{{link}}へアクセス",
         "register_2": "プロジェクトがない場合はプロジェクトを作成",
         "register_2": "プロジェクトがない場合はプロジェクトを作成",
@@ -573,7 +531,7 @@
         "updated_twitter": "Twitter OAuth を更新しました"
         "updated_twitter": "Twitter OAuth を更新しました"
       },
       },
       "GitHub": {
       "GitHub": {
-        "enable_github":"GitHub OAuth を有効にする",
+        "enable_github": "GitHub OAuth を有効にする",
         "name": "GitHub OAuth",
         "name": "GitHub OAuth",
         "register_1": "{{link}} へアクセス",
         "register_1": "{{link}} へアクセス",
         "register_2": "\"Authorization callback URL\"を<code>{{url}}</code>としてGrowiを登録",
         "register_2": "\"Authorization callback URL\"を<code>{{url}}</code>としてGrowiを登録",
@@ -605,7 +563,6 @@
       "attrMapLastName": "名"
       "attrMapLastName": "名"
     }
     }
   },
   },
-
   "markdown_setting": {
   "markdown_setting": {
     "line_break_setting": "Line Break設定",
     "line_break_setting": "Line Break設定",
     "line_break_setting_desc": "Line Breakの設定を変更できます。",
     "line_break_setting_desc": "Line Breakの設定を変更できます。",
@@ -638,7 +595,6 @@
     "updated_presentation": "プレゼンテーション設定を更新しました",
     "updated_presentation": "プレゼンテーション設定を更新しました",
     "updated_xss": "XSS設定を更新しました"
     "updated_xss": "XSS設定を更新しました"
   },
   },
-
   "notification_setting": {
   "notification_setting": {
     "notification_list": "通知設定の一覧",
     "notification_list": "通知設定の一覧",
     "add_notification": "通知設定の追加",
     "add_notification": "通知設定の追加",
@@ -658,9 +614,8 @@
       "ifttt_link": "IFTTT でメールトリガの新しいアプレットを作る"
       "ifttt_link": "IFTTT でメールトリガの新しいアプレットを作る"
     }
     }
   },
   },
-
   "customize_page": {
   "customize_page": {
-    "recommended":"おすすめ",
+    "recommended": "おすすめ",
     "Behavior": "動作",
     "Behavior": "動作",
     "Layout": "レイアウト",
     "Layout": "レイアウト",
     "Function": "機能",
     "Function": "機能",
@@ -697,32 +652,31 @@
     "update_customHeader_success": "カスタムHTMLヘッダーを更新しました",
     "update_customHeader_success": "カスタムHTMLヘッダーを更新しました",
     "update_customCss_success": "カスタムCSSを更新しました",
     "update_customCss_success": "カスタムCSSを更新しました",
     "update_script_success": "カスタムスクリプトを更新しました",
     "update_script_success": "カスタムスクリプトを更新しました",
-    "layout_description":{
-      "growi_title":"シンプル・明瞭",
-      "growi_text1":"全画面レイアウトで、余白は少なくなります。",
-      "growi_text2":"コメントはページの下部に表示されます。",
-      "growi_text3":"ページ情報は下部に表示されます。",
-      "kibela_title":"閲覧重視の構造",
-      "kibela_text1":"コンテンツが中心に表示されます。",
-      "kibela_text2":"コメントはページの下部に表示されます。",
-      "kibela_text3":"ページ情報は下部に表示されます。",
-      "crowi_title":"ビュー・コントロールの分離",
-      "crowi_text1":"サイドバーを開くと情報が表示されます。",
-      "crowi_text2":"コメントはサイドバーに表示されます。",
-      "crowi_text3":"ページ情報はサイドバーに表示されます。"
+    "layout_description": {
+      "growi_title": "シンプル・明瞭",
+      "growi_text1": "全画面レイアウトで、余白は少なくなります。",
+      "growi_text2": "コメントはページの下部に表示されます。",
+      "growi_text3": "ページ情報は下部に表示されます。",
+      "kibela_title": "閲覧重視の構造",
+      "kibela_text1": "コンテンツが中心に表示されます。",
+      "kibela_text2": "コメントはページの下部に表示されます。",
+      "kibela_text3": "ページ情報は下部に表示されます。",
+      "crowi_title": "ビュー・コントロールの分離",
+      "crowi_text1": "サイドバーを開くと情報が表示されます。",
+      "crowi_text2": "コメントはサイドバーに表示されます。",
+      "crowi_text3": "ページ情報はサイドバーに表示されます。"
     },
     },
-    "behavior_description":{
-      "growi_text1":"<code>/page</code>と<code>/page/</code>どちらのパスも同じページを表示します。",
-      "growi_text2":"<code>/nonexistent_page</code> では編集フォームを表示します",
-      "growi_text3":"<b>GROWI Enhanced Layout</b>では全てのページが配下のページリストを表示します",
-      "crowi_text1":"<code>/page</code> ではページを表示します。",
-      "crowi_text2":"<code>/page/</code> では配下のページを表示します。",
-      "crowi_text3":"<code>/page/</code>がポータルに適応している場合、ポータルページと配下のページリストを表示します。",
-      "crowi_text4":"<code>/nonexistent_page</code> では編集フォームを表示します",
-      "crowi_text5":"<code>/nonexistent_page</code> では配下のページリストを表示します。"
+    "behavior_description": {
+      "growi_text1": "<code>/page</code>と<code>/page/</code>どちらのパスも同じページを表示します。",
+      "growi_text2": "<code>/nonexistent_page</code> では編集フォームを表示します",
+      "growi_text3": "<b>GROWI Enhanced Layout</b>では全てのページが配下のページリストを表示します",
+      "crowi_text1": "<code>/page</code> ではページを表示します。",
+      "crowi_text2": "<code>/page/</code> では配下のページを表示します。",
+      "crowi_text3": "<code>/page/</code>がポータルに適応している場合、ポータルページと配下のページリストを表示します。",
+      "crowi_text4": "<code>/nonexistent_page</code> では編集フォームを表示します",
+      "crowi_text5": "<code>/nonexistent_page</code> では配下のページリストを表示します。"
     }
     }
   },
   },
-
   "user_management": {
   "user_management": {
     "target_user": "対象ユーザー",
     "target_user": "対象ユーザー",
     "new_password": "新しいパスワード",
     "new_password": "新しいパスワード",
@@ -766,8 +720,12 @@
     "remove_user_success": "{{username}}を削除しました",
     "remove_user_success": "{{username}}を削除しました",
     "remove_external_user_success": "{{accountId}}を削除しました "
     "remove_external_user_success": "{{accountId}}を削除しました "
   },
   },
-
   "user_group_management": {
   "user_group_management": {
+    "search_option": "検索オプション",
+    "enable_option": "{{option}}を有効にする",
+    "forward_match": "前方一致",
+    "partial_match": "部分一致",
+    "backward_match": "後方一致",
     "group_list": "グループ一覧",
     "group_list": "グループ一覧",
     "back_to_list": "グループ一覧に戻る",
     "back_to_list": "グループ一覧に戻る",
     "basic_info": "基本情報",
     "basic_info": "基本情報",
@@ -790,7 +748,6 @@
     "no_pages": "グループが閲覧権限を保有するページはありません",
     "no_pages": "グループが閲覧権限を保有するページはありません",
     "remove_from_group": "グループから外す"
     "remove_from_group": "グループから外す"
   },
   },
-
   "importer_management": {
   "importer_management": {
     "beta_warning": "この機能はベータ版です",
     "beta_warning": "この機能はベータ版です",
     "import_from": "{{from}} からインポート",
     "import_from": "{{from}} からインポート",
@@ -849,13 +806,12 @@
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "page_skip": "既に GROWI 側に同名のページが存在する場合、そのページはスキップされます",
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
     "Directory_hierarchy_tag": "ディレクトリ階層タグ"
   },
   },
-
-  "full_text_search_management":{
-    "elasticsearch_management":"Elasticsearch 管理",
-    "build_button":"インデックスのリビルド",
-    "rebuild_description_1":"Build Now ボタンを押すと全てのページのインデックスを削除し、作り直します。",
-    "rebuild_description_2":"この作業には数秒かかります。",
-    "rebuild_description_3":""
+  "full_text_search_management": {
+    "elasticsearch_management": "Elasticsearch 管理",
+    "build_button": "インデックスのリビルド",
+    "rebuild_description_1": "Build Now ボタンを押すと全てのページのインデックスを削除し、作り直します。",
+    "rebuild_description_2": "この作業には数秒かかります。",
+    "rebuild_description_3": ""
   },
   },
   "export_management": {
   "export_management": {
     "exporting_collection_list": "エクスポート中のコレクション",
     "exporting_collection_list": "エクスポート中のコレクション",

+ 25 - 42
src/client/js/components/Admin/MarkdownSetting/WhiteListInput.jsx

@@ -10,78 +10,62 @@ import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 
 
 class WhiteListInput extends React.Component {
 class WhiteListInput extends React.Component {
 
 
-  renderRecommendTagBtn() {
-    const { t, adminMarkDownContainer } = this.props;
+  constructor(props) {
+    super(props);
 
 
-    return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ tagWhiteList: tags }) }}>
-        { t('markdown_setting.import_recommended', 'tags') }
-      </p>
-    );
-  }
+    this.tagWhiteList = React.createRef();
+    this.attrWhiteList = React.createRef();
 
 
-  renderRecommendAttrBtn() {
-    const { t, adminMarkDownContainer } = this.props;
-
-    return (
-      <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={() => { adminMarkDownContainer.setState({ attrWhiteList: attrs }) }}>
-        { t('markdown_setting.import_recommended', 'Attrs') }
-      </p>
-    );
+    this.onClickRecommendTagButton = this.onClickRecommendTagButton.bind(this);
+    this.onClickRecommendAttrButton = this.onClickRecommendAttrButton.bind(this);
   }
   }
 
 
-  renderTagValue() {
-    const { customizable, adminMarkDownContainer } = this.props;
-
-    if (customizable) {
-      return adminMarkDownContainer.state.tagWhiteList;
-    }
-
-    return tags;
+  onClickRecommendTagButton() {
+    this.tagWhiteList.current.value = tags;
+    this.props.adminMarkDownContainer.setState({ tagWhiteList: tags });
   }
   }
 
 
-  renderAttrValue() {
-    const { customizable, adminMarkDownContainer } = this.props;
-
-    if (customizable) {
-      return adminMarkDownContainer.state.attrWhiteList;
-    }
-
-    return attrs;
+  onClickRecommendAttrButton() {
+    this.attrWhiteList.current.value = attrs;
+    this.props.adminMarkDownContainer.setState({ attrWhiteList: attrs });
   }
   }
 
 
   render() {
   render() {
-    const { t, customizable, adminMarkDownContainer } = this.props;
+    const { t, adminMarkDownContainer } = this.props;
 
 
     return (
     return (
       <>
       <>
         <div className="m-t-15">
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
           <div className="d-flex justify-content-between">
-            { t('markdown_setting.Tag names') }
-            {customizable && this.renderRecommendTagBtn()}
+            {t('markdown_setting.Tag names')}
+            <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendTagButton}>
+              {t('markdown_setting.import_recommended', 'tags')}
+            </p>
           </div>
           </div>
           <textarea
           <textarea
             className="form-control xss-list"
             className="form-control xss-list"
             name="recommendedTags"
             name="recommendedTags"
             rows="6"
             rows="6"
             cols="40"
             cols="40"
-            readOnly={!customizable}
-            defaultValue={this.renderTagValue()}
+            ref={this.tagWhiteList}
+            defaultValue={adminMarkDownContainer.state.tagWhiteList}
             onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
             onChange={(e) => { adminMarkDownContainer.setState({ tagWhiteList: e.target.value }) }}
           />
           />
         </div>
         </div>
         <div className="m-t-15">
         <div className="m-t-15">
           <div className="d-flex justify-content-between">
           <div className="d-flex justify-content-between">
-            { t('markdown_setting.Tag attributes') }
-            {customizable && this.renderRecommendAttrBtn()}
+            {t('markdown_setting.Tag attributes')}
+            <p id="btn-import-tags" className="btn btn-xs btn-primary" onClick={this.onClickRecommendAttrButton}>
+              {t('markdown_setting.import_recommended', 'Attrs')}
+            </p>
           </div>
           </div>
           <textarea
           <textarea
             className="form-control xss-list"
             className="form-control xss-list"
             name="recommendedAttrs"
             name="recommendedAttrs"
             rows="6"
             rows="6"
             cols="40"
             cols="40"
-            readOnly={!customizable}
-            defaultValue={this.renderAttrValue()}
+            ref={this.attrWhiteList}
+            defaultValue={adminMarkDownContainer.state.attrWhiteList}
             onChange={(e) => { adminMarkDownContainer.setState({ attrWhiteList: e.target.value }) }}
             onChange={(e) => { adminMarkDownContainer.setState({ attrWhiteList: e.target.value }) }}
           />
           />
         </div>
         </div>
@@ -100,7 +84,6 @@ WhiteListInput.propTypes = {
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   appContainer: PropTypes.instanceOf(AppContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
   adminMarkDownContainer: PropTypes.instanceOf(AdminMarkDownContainer).isRequired,
 
 
-  customizable: PropTypes.bool.isRequired,
 };
 };
 
 
 export default withTranslation()(WhiteListWrapper);
 export default withTranslation()(WhiteListWrapper);

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

@@ -5,6 +5,7 @@ import loggerFactory from '@alias/logger';
 
 
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
+import { tags, attrs } from '../../../../../lib/service/xss/recommended-whitelist';
 
 
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
 import AdminMarkDownContainer from '../../../services/AdminMarkDownContainer';
@@ -39,7 +40,7 @@ class XssForm extends React.Component {
     const { xssOption } = adminMarkDownContainer.state;
     const { xssOption } = adminMarkDownContainer.state;
 
 
     return (
     return (
-      <fieldset className="form-group col-xs-12 my-3">
+      <fieldset className="row col-xs-12 my-3">
         <div className="col-xs-4 radio radio-primary">
         <div className="col-xs-4 radio radio-primary">
           <input
           <input
             type="radio"
             type="radio"
@@ -49,9 +50,9 @@ class XssForm extends React.Component {
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 1 }) }}
           />
           />
           <label htmlFor="xssOption1">
           <label htmlFor="xssOption1">
-            <p className="font-weight-bold">{ t('markdown_setting.Ignore all tags') }</p>
+            <p className="font-weight-bold">{t('markdown_setting.Ignore all tags')}</p>
             <div className="m-t-15">
             <div className="m-t-15">
-              { t('markdown_setting.Ignore all tags desc') }
+              {t('markdown_setting.Ignore all tags desc')}
             </div>
             </div>
           </label>
           </label>
         </div>
         </div>
@@ -65,8 +66,33 @@ class XssForm extends React.Component {
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 2 }) }}
           />
           />
           <label htmlFor="xssOption2">
           <label htmlFor="xssOption2">
-            <p className="font-weight-bold">{ t('markdown_setting.Recommended setting') }</p>
-            <WhiteListInput customizable={false} />
+            <p className="font-weight-bold">{t('markdown_setting.Recommended setting')}</p>
+            <div className="m-t-15">
+              <div className="d-flex justify-content-between">
+                {t('markdown_setting.Tag names')}
+              </div>
+              <textarea
+                className="form-control xss-list"
+                name="recommendedTags"
+                rows="6"
+                cols="40"
+                readOnly
+                defaultValue={tags}
+              />
+            </div>
+            <div className="m-t-15">
+              <div className="d-flex justify-content-between">
+                {t('markdown_setting.Tag attributes')}
+              </div>
+              <textarea
+                className="form-control xss-list"
+                name="recommendedAttrs"
+                rows="6"
+                cols="40"
+                readOnly
+                defaultValue={attrs}
+              />
+            </div>
           </label>
           </label>
         </div>
         </div>
 
 
@@ -79,8 +105,8 @@ class XssForm extends React.Component {
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
             onChange={() => { adminMarkDownContainer.setState({ xssOption: 3 }) }}
           />
           />
           <label htmlFor="xssOption3">
           <label htmlFor="xssOption3">
-            <p className="font-weight-bold">{ t('markdown_setting.Custom Whitelist') }</p>
-            <WhiteListInput customizable />
+            <p className="font-weight-bold">{t('markdown_setting.Custom Whitelist')}</p>
+            <WhiteListInput />
           </label>
           </label>
         </div>
         </div>
       </fieldset>
       </fieldset>
@@ -106,7 +132,7 @@ class XssForm extends React.Component {
                   onChange={adminMarkDownContainer.switchEnableXss}
                   onChange={adminMarkDownContainer.switchEnableXss}
                 />
                 />
                 <label htmlFor="XssEnable">
                 <label htmlFor="XssEnable">
-                  { t('markdown_setting.Enable XSS prevention') }
+                  {t('markdown_setting.Enable XSS prevention')}
                 </label>
                 </label>
               </div>
               </div>
             </div>
             </div>

+ 37 - 0
src/client/js/components/Admin/UserGroupDetail/CheckBoxForSerchUserOption.jsx

@@ -0,0 +1,37 @@
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+class CheckBoxForSerchUserOption extends React.Component {
+
+  render() {
+    const { t, option } = this.props;
+    return (
+      <div className="checkbox checkbox-info" key={`isAlso${option}Searched`}>
+        <input
+          type="checkbox"
+          id={`isAlso${option}Searched`}
+          className="form-check-input"
+          checked={this.props.checked}
+          onChange={this.props.onChange}
+        />
+        <label className="text-capitalize form-check-label ml-3" htmlFor={`isAlso${option}Searched`}>
+          {t('user_group_management.enable_option', { option })}
+        </label>
+      </div>
+    );
+  }
+
+}
+
+
+CheckBoxForSerchUserOption.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+  option: PropTypes.string.isRequired,
+  checked: PropTypes.bool.isRequired,
+  onChange: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(CheckBoxForSerchUserOption);

+ 37 - 0
src/client/js/components/Admin/UserGroupDetail/RadioButtonForSerchUserOption.jsx

@@ -0,0 +1,37 @@
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { withTranslation } from 'react-i18next';
+
+class RadioButtonForSerchUserOption extends React.Component {
+
+  render() {
+    const { t, searchType } = this.props;
+    return (
+      <div className="radio" key={`${searchType}Match`}>
+        <input
+          type="radio"
+          id={`${searchType}Match`}
+          className="form-check-radio"
+          checked={this.props.checked}
+          onChange={this.props.onChange}
+        />
+        <label className="text-capitalize form-check-label ml-3" htmlFor={`${searchType}Match`}>
+          {t(`user_group_management.${searchType}_match`)}
+        </label>
+      </div>
+    );
+  }
+
+}
+
+
+RadioButtonForSerchUserOption.propTypes = {
+  t: PropTypes.func.isRequired, // i18next
+
+  searchType: PropTypes.string.isRequired,
+  checked: PropTypes.bool.isRequired,
+  onChange: PropTypes.func.isRequired,
+};
+
+export default withTranslation()(RadioButtonForSerchUserOption);

+ 1 - 1
src/client/js/components/Admin/UserGroupDetail/UserGroupPageList.jsx

@@ -40,7 +40,7 @@ class UserGroupPageList extends React.Component {
       const { total, pages } = res.data;
       const { total, pages } = res.data;
 
 
       this.setState({
       this.setState({
-        total,
+        total: total || 0,
         activePage: pageNum,
         activePage: pageNum,
         currentPages: pages,
         currentPages: pages,
       });
       });

+ 105 - 24
src/client/js/components/Admin/UserGroupDetail/UserGroupUserFormByInput.jsx

@@ -2,10 +2,13 @@ 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 { AsyncTypeahead } from 'react-bootstrap-typeahead';
+import { debounce } from 'throttle-debounce';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
 import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
+import UserPicture from '../../User/UserPicture';
 
 
 class UserGroupUserFormByInput extends React.Component {
 class UserGroupUserFormByInput extends React.Component {
 
 
@@ -13,55 +16,133 @@ class UserGroupUserFormByInput extends React.Component {
     super(props);
     super(props);
 
 
     this.state = {
     this.state = {
-      username: '',
+      keyword: '',
+      inputUser: '',
+      applicableUsers: [],
+      isLoading: false,
+      searchError: null,
     };
     };
 
 
     this.xss = window.xss;
     this.xss = window.xss;
 
 
-    this.changeUsername = this.changeUsername.bind(this);
     this.addUserBySubmit = this.addUserBySubmit.bind(this);
     this.addUserBySubmit = this.addUserBySubmit.bind(this);
     this.validateForm = this.validateForm.bind(this);
     this.validateForm = this.validateForm.bind(this);
-  }
+    this.handleChange = this.handleChange.bind(this);
+    this.handleSearch = this.handleSearch.bind(this);
+    this.onKeyDown = this.onKeyDown.bind(this);
+    this.renderMenuItemChildren = this.renderMenuItemChildren.bind(this);
 
 
-  changeUsername(e) {
-    this.setState({ username: e.target.value });
+    this.searhApplicableUsersDebounce = debounce(1000, this.searhApplicableUsers);
   }
   }
 
 
-  async addUserBySubmit(e) {
-    e.preventDefault();
-    const { username } = this.state;
+  async addUserBySubmit() {
+    if (this.state.inputUser.length === 0) { return }
+    const userName = this.state.inputUser[0].username;
 
 
     try {
     try {
-      await this.props.userGroupDetailContainer.addUserByUsername(username);
-      toastSuccess(`Added "${this.xss.process(username)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`);
-      this.setState({ username: '' });
+      await this.props.userGroupDetailContainer.addUserByUsername(userName);
+      toastSuccess(`Added "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`);
+      this.setState({ inputUser: '' });
     }
     }
     catch (err) {
     catch (err) {
-      toastError(new Error(`Unable to add "${this.xss.process(username)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`));
+      toastError(new Error(`Unable to add "${this.xss.process(userName)}" to "${this.xss.process(this.props.userGroupDetailContainer.state.userGroup.name)}"`));
     }
     }
   }
   }
 
 
   validateForm() {
   validateForm() {
-    return this.state.username !== '';
+    return this.state.inputUser !== '';
+  }
+
+  async searhApplicableUsers() {
+    try {
+      const users = await this.props.userGroupDetailContainer.fetchApplicableUsers(this.state.keyword);
+      this.setState({ applicableUsers: users, isLoading: false });
+    }
+    catch (err) {
+      toastError(err);
+    }
+  }
+
+  /**
+   * Reflect when forecast is clicked
+   * @param {object} inputUser
+   */
+  handleChange(inputUser) {
+    this.setState({ inputUser });
+  }
+
+  handleSearch(keyword) {
+
+    if (keyword === '') {
+      return;
+    }
+
+    this.setState({ keyword, isLoading: true });
+    this.searhApplicableUsersDebounce();
+  }
+
+  onKeyDown(event) {
+    // 13 is Enter key
+    if (event.keyCode === 13) {
+      this.addUserBySubmit();
+    }
+  }
+
+  renderMenuItemChildren(option) {
+    const { userGroupDetailContainer } = this.props;
+    const user = option;
+    return (
+      <React.Fragment>
+        <UserPicture user={user} size="sm" withoutLink />
+        <strong className="ml-2">{user.username}</strong>
+        {userGroupDetailContainer.state.isAlsoNameSearched && <span className="ml-2">{user.name}</span>}
+        {userGroupDetailContainer.state.isAlsoMailSearched && <span className="ml-2">{user.email}</span>}
+      </React.Fragment>
+    );
+  }
+
+  getEmptyLabel() {
+    return (this.state.searchError !== null) && 'Error on searching.';
   }
   }
 
 
   render() {
   render() {
     const { t } = this.props;
     const { t } = this.props;
 
 
+    const inputProps = { autoComplete: 'off' };
+
     return (
     return (
-      <form className="form-inline" onSubmit={this.addUserBySubmit}>
-        <div className="form-group">
-          <input
-            type="text"
-            name="username"
-            className="form-control input-sm"
-            placeholder={t('username')}
-            value={this.state.username}
-            onChange={this.changeUsername}
+      <div className="row">
+        <div className="col-xs-8 pr-0">
+          <AsyncTypeahead
+            {...this.props}
+            id="name-typeahead-asynctypeahead"
+            ref={(c) => { this.typeahead = c }}
+            inputProps={inputProps}
+            isLoading={this.state.isLoading}
+            labelKey={user => `${user.username} ${user.name} ${user.email}`}
+            minLength={0}
+            options={this.state.applicableUsers} // Search result
+            searchText={(this.state.isLoading ? 'Searching...' : this.getEmptyLabel())}
+            renderMenuItemChildren={this.renderMenuItemChildren}
+            align="left"
+            onChange={this.handleChange}
+            onSearch={this.handleSearch}
+            onKeyDown={this.onKeyDown}
+            caseSensitive={false}
+            clearButton
           />
           />
         </div>
         </div>
-        <button type="submit" className="btn btn-sm btn-success" disabled={!this.validateForm()}>{ t('add') }</button>
-      </form>
+        <div className="col-xs-2 pl-0">
+          <button
+            type="button"
+            className="btn btn-sm btn-success"
+            disabled={!this.validateForm()}
+            onClick={this.addUserBySubmit}
+          >
+            {t('add')}
+          </button>
+        </div>
+      </div>
     );
     );
   }
   }
 
 

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

@@ -7,6 +7,8 @@ import UserGroupUserFormByInput from './UserGroupUserFormByInput';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import AppContainer from '../../../services/AppContainer';
 import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
 import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
+import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 
 
 class UserGroupUserModal extends React.Component {
 class UserGroupUserModal extends React.Component {
 
 
@@ -16,10 +18,54 @@ class UserGroupUserModal extends React.Component {
     return (
     return (
       <Modal show={userGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={userGroupDetailContainer.closeUserGroupUserModal}>
       <Modal show={userGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={userGroupDetailContainer.closeUserGroupUserModal}>
         <Modal.Header closeButton>
         <Modal.Header closeButton>
-          <Modal.Title>{ t('user_group_management.add_user') }</Modal.Title>
+          <Modal.Title>{t('user_group_management.add_user')}</Modal.Title>
         </Modal.Header>
         </Modal.Header>
         <Modal.Body>
         <Modal.Body>
-          <UserGroupUserFormByInput />
+          <div className="p-3">
+            <UserGroupUserFormByInput />
+          </div>
+          <h2 className="border-bottom">{t('user_group_management.search_option')}</h2>
+          <div className="row mt-4">
+            <div className="col-xs-6">
+              <div className="mb-5">
+                <CheckBoxForSerchUserOption
+                  option="Mail"
+                  checked={userGroupDetailContainer.state.isAlsoMailSearched}
+                  onChange={userGroupDetailContainer.switchIsAlsoMailSearched}
+                />
+              </div>
+              <div className="mb-5">
+                <CheckBoxForSerchUserOption
+                  option="Name"
+                  checked={userGroupDetailContainer.state.isAlsoNameSearched}
+                  onChange={userGroupDetailContainer.switchIsAlsoNameSearched}
+                />
+              </div>
+            </div>
+            <div className="col-xs-6">
+              <div className="mb-5">
+                <RadioButtonForSerchUserOption
+                  searchType="forward"
+                  checked={userGroupDetailContainer.state.searchType === 'forward'}
+                  onChange={() => { userGroupDetailContainer.switchSearchType('forward') }}
+                />
+              </div>
+              <div className="mb-5">
+                <RadioButtonForSerchUserOption
+                  searchType="partial"
+                  checked={userGroupDetailContainer.state.searchType === 'partial'}
+                  onChange={() => { userGroupDetailContainer.switchSearchType('partial') }}
+                />
+              </div>
+              <div className="mb-5">
+                <RadioButtonForSerchUserOption
+                  searchType="backward"
+                  checked={userGroupDetailContainer.state.searchType === 'backword'}
+                  onChange={() => { userGroupDetailContainer.switchSearchType('backword') }}
+                />
+              </div>
+            </div>
+          </div>
         </Modal.Body>
         </Modal.Body>
       </Modal>
       </Modal>
     );
     );

+ 6 - 2
src/client/js/services/AdminMarkDownContainer.js

@@ -106,12 +106,16 @@ export default class AdminMarkDownContainer extends Container {
    * Update Xss Setting
    * Update Xss Setting
    */
    */
   async updateXssSetting() {
   async updateXssSetting() {
+    let { tagWhiteList, attrWhiteList } = this.state;
+
+    tagWhiteList = Array.isArray(tagWhiteList) ? tagWhiteList : tagWhiteList.split(',');
+    attrWhiteList = Array.isArray(attrWhiteList) ? attrWhiteList : attrWhiteList.split(',');
 
 
     const response = await this.appContainer.apiv3.put('/markdown-setting/xss', {
     const response = await this.appContainer.apiv3.put('/markdown-setting/xss', {
       isEnabledXss: this.state.isEnabledXss,
       isEnabledXss: this.state.isEnabledXss,
       xssOption: this.state.xssOption,
       xssOption: this.state.xssOption,
-      tagWhiteList: this.state.tagWhiteList,
-      attrWhiteList: this.state.attrWhiteList,
+      tagWhiteList,
+      attrWhiteList,
     });
     });
 
 
     return response;
     return response;

+ 48 - 0
src/client/js/services/UserGroupDetailContainer.js

@@ -24,10 +24,15 @@ export default class UserGroupDetailContainer extends Container {
       userGroupRelations: [],
       userGroupRelations: [],
       relatedPages: [],
       relatedPages: [],
       isUserGroupUserModalOpen: false,
       isUserGroupUserModalOpen: false,
+      searchType: 'partial',
+      isAlsoMailSearched: false,
+      isAlsoNameSearched: false,
     };
     };
 
 
     this.init();
     this.init();
 
 
+    this.switchIsAlsoMailSearched = this.switchIsAlsoMailSearched.bind(this);
+    this.switchIsAlsoNameSearched = this.switchIsAlsoNameSearched.bind(this);
     this.openUserGroupUserModal = this.openUserGroupUserModal.bind(this);
     this.openUserGroupUserModal = this.openUserGroupUserModal.bind(this);
     this.closeUserGroupUserModal = this.closeUserGroupUserModal.bind(this);
     this.closeUserGroupUserModal = this.closeUserGroupUserModal.bind(this);
     this.addUserByUsername = this.addUserByUsername.bind(this);
     this.addUserByUsername = this.addUserByUsername.bind(this);
@@ -65,6 +70,27 @@ export default class UserGroupDetailContainer extends Container {
     }
     }
   }
   }
 
 
+  /**
+   * switch isAlsoMailSearched
+   */
+  switchIsAlsoMailSearched() {
+    this.setState({ isAlsoMailSearched: !this.state.isAlsoMailSearched });
+  }
+
+  /**
+   * switch isAlsoNameSearched
+   */
+  switchIsAlsoNameSearched() {
+    this.setState({ isAlsoNameSearched: !this.state.isAlsoNameSearched });
+  }
+
+  /**
+   * switch searchType
+   */
+  switchSearchType(searchType) {
+    this.setState({ searchType });
+  }
+
   /**
   /**
    * update user group
    * update user group
    *
    *
@@ -99,6 +125,24 @@ export default class UserGroupDetailContainer extends Container {
     await this.setState({ isUserGroupUserModalOpen: false });
     await this.setState({ isUserGroupUserModalOpen: false });
   }
   }
 
 
+  /**
+   * search user for invitation
+   * @param {string} username username of the user to be searched
+   */
+  async fetchApplicableUsers(searchWord) {
+    const res = await this.appContainer.apiv3.get(`/user-groups/${this.state.userGroup._id}/unrelated-users`, {
+      searchWord,
+      searchType: this.state.searchType,
+      isAlsoMailSearched: this.state.isAlsoMailSearched,
+      isAlsoNameSearched: this.state.isAlsoNameSearched,
+    });
+
+    const { users } = res.data;
+
+    return users;
+  }
+
+
   /**
   /**
    * update user group
    * update user group
    *
    *
@@ -107,6 +151,10 @@ export default class UserGroupDetailContainer extends Container {
    */
    */
   async addUserByUsername(username) {
   async addUserByUsername(username) {
     const res = await this.appContainer.apiv3.post(`/user-groups/${this.state.userGroup._id}/users/${username}`);
     const res = await this.appContainer.apiv3.post(`/user-groups/${this.state.userGroup._id}/users/${username}`);
+
+    // do not add users for ducaplicate
+    if (res.data.userGroupRelation == null) { return }
+
     const { userGroupRelation } = res.data;
     const { userGroupRelation } = res.data;
 
 
     this.setState((prevState) => {
     this.setState((prevState) => {

+ 23 - 5
src/server/models/user-group-relation.js

@@ -194,15 +194,33 @@ class UserGroupRelation {
    * @returns {Promise<User>}
    * @returns {Promise<User>}
    * @memberof UserGroupRelation
    * @memberof UserGroupRelation
    */
    */
-  static findUserByNotRelatedGroup(userGroup) {
+  static findUserByNotRelatedGroup(userGroup, queryOptions) {
     const User = UserGroupRelation.crowi.model('User');
     const User = UserGroupRelation.crowi.model('User');
+    let searchWord = new RegExp(`${queryOptions.searchWord}`);
+    switch (queryOptions.searchType) {
+      case 'forward':
+        searchWord = new RegExp(`^${queryOptions.searchWord}`);
+        break;
+      case 'backword':
+        searchWord = new RegExp(`${queryOptions.searchWord}$`);
+        break;
+    }
+    const searthField = [
+      { username: searchWord },
+    ];
+    if (queryOptions.isAlsoMailSearched === 'true') { searthField.push({ email: searchWord }) }
+    if (queryOptions.isAlsoNameSearched === 'true') { searthField.push({ name: searchWord }) }
 
 
     return this.findAllRelationForUserGroup(userGroup)
     return this.findAllRelationForUserGroup(userGroup)
       .then((relations) => {
       .then((relations) => {
         const relatedUserIds = relations.map((relation) => {
         const relatedUserIds = relations.map((relation) => {
           return relation.relatedUser.id;
           return relation.relatedUser.id;
         });
         });
-        const query = { _id: { $nin: relatedUserIds }, status: User.STATUS_ACTIVE };
+        const query = {
+          _id: { $nin: relatedUserIds },
+          status: User.STATUS_ACTIVE,
+          $or: searthField,
+        };
 
 
         debug('findUserByNotRelatedGroup ', query);
         debug('findUserByNotRelatedGroup ', query);
         return User.find(query).exec();
         return User.find(query).exec();
@@ -213,15 +231,15 @@ class UserGroupRelation {
    * get if the user has relation for group
    * get if the user has relation for group
    *
    *
    * @static
    * @static
-   * @param {User} userData
    * @param {UserGroup} userGroup
    * @param {UserGroup} userGroup
+   * @param {User} user
    * @returns {Promise<boolean>} is user related for group(or not)
    * @returns {Promise<boolean>} is user related for group(or not)
    * @memberof UserGroupRelation
    * @memberof UserGroupRelation
    */
    */
-  static isRelatedUserForGroup(userData, userGroup) {
+  static isRelatedUserForGroup(userGroup, user) {
     const query = {
     const query = {
       relatedGroup: userGroup.id,
       relatedGroup: userGroup.id,
-      relatedUser: userData.id,
+      relatedUser: user.id,
     };
     };
 
 
     return this
     return this

+ 4 - 8
src/server/routes/apiv3/customize-setting.js

@@ -130,14 +130,10 @@ module.exports = (crowi) => {
    *            content:
    *            content:
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
-   *                  $ref: '#/components/schemas/CustomizeLayoutTheme'
-   *                  $ref: '#/components/schemas/CustomizeBehavior'
-   *                  $ref: '#/components/schemas/CustomizeFunction'
-   *                  $ref: '#/components/schemas/CustomizeHighlight'
-   *                  $ref: '#/components/schemas/CustomizeTitle'
-   *                  $ref: '#/components/schemas/CustomizeHeader'
-   *                  $ref: '#/components/schemas/CustomizeCss'
-   *                  $ref: '#/components/schemas/CustomizeScript'
+   *                  properties:
+   *                    customizeParams:
+   *                      type: object
+   *                      description: customize params
    */
    */
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
 
 

+ 5 - 6
src/server/routes/apiv3/markdown-setting.js

@@ -21,8 +21,8 @@ const validator = {
   ],
   ],
   xssSetting: [
   xssSetting: [
     body('isEnabledXss').isBoolean(),
     body('isEnabledXss').isBoolean(),
-    body('tagWhiteList').toArray(),
-    body('attrWhiteList').toArray(),
+    body('tagWhiteList').isArray(),
+    body('attrWhiteList').isArray(),
   ],
   ],
 };
 };
 
 
@@ -100,10 +100,9 @@ module.exports = (crowi) => {
    *              application/json:
    *              application/json:
    *                schema:
    *                schema:
    *                  properties:
    *                  properties:
-   *                    markdonwParams:
-   *                      $ref: '#/components/schemas/LineBreakParams'
-   *                      $ref: '#/components/schemas/PresentationParams'
-   *                      $ref: '#/components/schemas/XssParams'
+   *                    markdownParams:
+   *                      type: object
+   *                      description: markdown params
    */
    */
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/', loginRequiredStrictly, adminRequired, async(req, res) => {
     const markdownParams = {
     const markdownParams = {

+ 16 - 1
src/server/routes/apiv3/user-group.js

@@ -320,10 +320,17 @@ module.exports = (crowi) => {
    */
    */
   router.get('/:id/unrelated-users', loginRequiredStrictly, adminRequired, async(req, res) => {
   router.get('/:id/unrelated-users', loginRequiredStrictly, adminRequired, async(req, res) => {
     const { id } = req.params;
     const { id } = req.params;
+    const {
+      searchWord, searchType, isAlsoNameSearched, isAlsoMailSearched,
+    } = req.query;
+
+    const queryOptions = {
+      searchWord, searchType, isAlsoNameSearched, isAlsoMailSearched,
+    };
 
 
     try {
     try {
       const userGroup = await UserGroup.findById(id);
       const userGroup = await UserGroup.findById(id);
-      const users = await UserGroupRelation.findUserByNotRelatedGroup(userGroup);
+      const users = await UserGroupRelation.findUserByNotRelatedGroup(userGroup, queryOptions);
 
 
       return res.apiv3({ users });
       return res.apiv3({ users });
     }
     }
@@ -381,6 +388,14 @@ module.exports = (crowi) => {
         User.findUserByUsername(username),
         User.findUserByUsername(username),
       ]);
       ]);
 
 
+      // check for duplicate users in groups
+      const isRelatedUserForGroup = await UserGroupRelation.isRelatedUserForGroup(userGroup, user);
+
+      if (isRelatedUserForGroup) {
+        logger.warn('The user is already joined');
+        return res.apiv3();
+      }
+
       const userGroupRelation = await UserGroupRelation.createRelation(userGroup, user);
       const userGroupRelation = await UserGroupRelation.createRelation(userGroup, user);
       await userGroupRelation.populate('relatedUser', User.USER_PUBLIC_FIELDS).execPopulate();
       await userGroupRelation.populate('relatedUser', User.USER_PUBLIC_FIELDS).execPopulate();
 
 

+ 2 - 2
src/server/service/passport.js

@@ -3,7 +3,7 @@ const urljoin = require('url-join');
 const passport = require('passport');
 const passport = require('passport');
 const LocalStrategy = require('passport-local').Strategy;
 const LocalStrategy = require('passport-local').Strategy;
 const LdapStrategy = require('passport-ldapauth');
 const LdapStrategy = require('passport-ldapauth');
-const GoogleStrategy = require('passport-google-auth').Strategy;
+const GoogleStrategy = require('passport-google-oauth20').Strategy;
 const GitHubStrategy = require('passport-github').Strategy;
 const GitHubStrategy = require('passport-github').Strategy;
 const TwitterStrategy = require('passport-twitter').Strategy;
 const TwitterStrategy = require('passport-twitter').Strategy;
 const OidcStrategy = require('openid-client').Strategy;
 const OidcStrategy = require('openid-client').Strategy;
@@ -340,7 +340,7 @@ class PassportService {
     passport.use(
     passport.use(
       new GoogleStrategy(
       new GoogleStrategy(
         {
         {
-          clientId: configManager.getConfig('crowi', 'security:passport-google:clientId'),
+          clientID: configManager.getConfig('crowi', 'security:passport-google:clientId'),
           clientSecret: configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
           clientSecret: configManager.getConfig('crowi', 'security:passport-google:clientSecret'),
           callbackURL: (this.crowi.appService.getSiteUrl() != null)
           callbackURL: (this.crowi.appService.getSiteUrl() != null)
             ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/google/callback') // auto-generated with v3.2.4 and above
             ? urljoin(this.crowi.appService.getSiteUrl(), '/passport/google/callback') // auto-generated with v3.2.4 and above

Разница между файлами не показана из-за своего большого размера
+ 304 - 207
yarn.lock


Некоторые файлы не были показаны из-за большого количества измененных файлов