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

Merge pull request #1506 from weseek/feat/show-suggestions-for-joining-user-group

Feat/show suggestions for joining user group
Yuki Takei 6 лет назад
Родитель
Сommit
b9c5431f09

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

@@ -28,13 +28,11 @@
   "Page Path": "Page Path",
   "Category": "Category",
   "User": "User",
-  "status":"Status",
+  "status": "Status",
   "account_id": "Account Id",
-
   "Update": "Update",
   "Update Page": "Update Page",
   "Warning": "Warning",
-
   "Sign in": "Sign in",
   "Sign up is here": "Sign up",
   "Sign in is here": "Sign in",
@@ -44,30 +42,23 @@
   "Sign up with this Google Account": "Sign up with this Google Account",
   "Example": "Example",
   "Taro Yamada": "John Doe",
-
   "List View": "List",
   "Timeline View": "Timeline",
   "History": "History",
   "Presentation Mode": "Presentation",
-
   "username": "Username",
   "Created": "Created",
   "Last updated": "Updated",
   "Last_Login": "Last Login",
-
   "Share": "Share",
   "Share Link": "Share Link",
   "Markdown Link": "Markdown Link",
-
   "Create/Edit Template": "Create/Edit Template Page",
-
   "Unportalize": "Unportalize",
-
   "Go to this version": "View this version",
   "View diff": "View diff",
   "No diff": "No diff",
   "Shrink versions that have no diffs": "Shrink versions that have no diffs",
-
   "User ID": "User ID",
   "Home": "Home",
   "User Settings": "User Settings",
@@ -89,19 +80,15 @@
   "Show": "Show",
   "Hide": "Hide",
   "Disclose E-mail": "Disclose E-mail",
-
   "page exists": "this page already exists",
   "Error occurred": "Error occurred",
-
   "Create today's": "Create today's ...",
   "Memo": "memo",
   "Input page name": "Input page name",
   "Input page name (optional)": "Input page name (optional)",
   "New Page": "New Page",
   "Create under": "Create page under below:",
-
   "Table of Contents": "Table of Contents",
-
   "Management Wiki Home": "Management Wiki Home",
   "App Settings": "App Settings",
   "Site URL settings": "Site URL settings",
@@ -129,30 +116,24 @@
   "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": "Drafts",
   "Deleted Pages": "Deleted Pages",
   "Sign out": "Logout",
-
   "form_validation": {
     "required": "<code>%s</code> is required"
   },
-
   "installer": {
     "setup": "Setup",
     "create_initial_account": "Create an initial account",
     "initial_account_will_be_administrator_automatically": "The initial account will be administrator automatically.",
     "unavaliable_user_id": "This 'User ID' is unavailable."
   },
-
   "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"
   },
-
   "page_register": {
     "notice": {
       "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."
     }
   },
-
   "page_me": {
     "form_help": {
       "profile_image1": "Image upload settings not completed.",
@@ -177,10 +157,8 @@
       "update_token1": "You can update to generate a new API Token.",
       "update_token2": "You will need to update the API Token in any existing processes."
     },
-    "form_help": {
-    }
+    "form_help": {}
   },
-
   "Password": "Password",
   "Password Settings": "Password Settings",
   "Set new Password": "Set new Password",
@@ -189,14 +167,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 Settings",
   "API Token Settings": "API Token Settings",
   "Current API Token": "Current API Token",
   "Update API Token": "Update API Token",
-
   "header_search_box": {
     "label": {
       "This tree": "This tree"
@@ -205,7 +180,6 @@
       "This tree": "Only children of this tree"
     }
   },
-
   "copy_to_clipboard": {
     "Copy to clipboard": "Copy to clipboard",
     "Page path": "Page path",
@@ -213,7 +187,6 @@
     "Page path and parmanent link": "Page path and parmanent link",
     "Markdown link": "Markdown link"
   },
-
   "search_help": {
     "title": "Searching Help",
     "and": {
@@ -243,7 +216,6 @@
   "search": {
     "search page bodies": "Hit [Enter] key to full-text search"
   },
-
   "page_page": {
     "notice": {
       "version": "This is not the current version.",
@@ -254,7 +226,6 @@
       "restricted": "Access to this page is restricted"
     }
   },
-
   "page_edit": {
     "Show active line": "Show active line",
     "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."
     }
   },
-
   "page_api_error": {
     "notfound_or_forbidden": "Original page is not found or forbidden.",
     "already_exists": "New page is already exists.",
     "outdated": "Page is updated someone and now outdated.",
     "user_not_admin": "Only admin user can delete completely"
   },
-
   "modal_rename": {
     "label": {
       "Move/Rename page": "Move/Rename page",
@@ -285,10 +254,8 @@
       "recursive": "Move/Rename children of under <code>%s</code> recursively"
     }
   },
-
   "Put Back": "Put Back",
   "Delete Completely": "Delete Completely",
-
   "modal_delete": {
     "delete_page": "Delete Page",
     "deleting_page": "Deleting Page",
@@ -298,7 +265,6 @@
     "recursively": "Delete children of under <code>%s</code> recursively.",
     "completely": "Delete completely instead of putting it into trash."
   },
-
   "modal_duplicate": {
     "label": {
       "Duplicate page": "Duplicate page",
@@ -306,7 +272,6 @@
       "Current page name": "Current page name"
     }
   },
-
   "modal_putback": {
     "label": {
       "Put Back Page": "Put Back Page",
@@ -316,7 +281,6 @@
       "recursively": "Put Back children of under <code>%s</code> recursively"
     }
   },
-
   "modal_shortcuts": {
     "global": {
       "title": "Global shortcuts",
@@ -339,7 +303,6 @@
       "Post": "Post"
     }
   },
-
   "template": {
     "modal_label": {
       "Create/Edit Template Page": "Create/Edit Template Page",
@@ -358,7 +321,6 @@
       "desc": "Applies to all decendant pages"
     }
   },
-
   "sandbox": {
     "header": "Header",
     "header_x": "Header {{index}}",
@@ -390,7 +352,6 @@
     "insert_image": "inserts an image",
     "open_sandbox": "Open Sandbox"
   },
-
   "admin_top": {
     "Management Wiki": "Management Wiki",
     "System Information": "System Information",
@@ -401,7 +362,6 @@
     "Specified version": "Specified version",
     "Installed version": "Installed version"
   },
-
   "app_setting": {
     "Site Name": "Site name",
     "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>.",
     "Confidential name": "Confidential name",
     "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",
     "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.",
@@ -421,7 +381,7 @@
     "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.",
     "From e-mail address": "From e-mail address",
-    "SMTP settings": "SMTP settings"  ,
+    "SMTP settings": "SMTP settings",
     "Host": "Host",
     "Port": "Port",
     "User": "User",
@@ -440,19 +400,18 @@
     "Disable": "Disable",
     "Use env var if empty": "If the value in the database is empty, the value of the environment variable <code>%s</code> is used."
   },
-
   "security_setting": {
-		"Security settings": "Security settings",
+    "Security settings": "Security settings",
     "Guest Users Access": "Guest Users Access",
     "Fixed by env var": "This is fixed by the env var <code>%s=%s</code>.",
     "Register limitation": "Register limitation",
     "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",
     "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.",
     "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",
@@ -463,11 +422,10 @@
     "admin_only": "Admin Only",
     "admin_and_author": "Admin and Author",
     "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 %s",
-    "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",
     "providerName": "Provider Name",
     "issuerHost": "Issuer Host",
@@ -596,8 +554,7 @@
       "security:passport-saml:attrMapFirstName": "First Name",
       "security:passport-saml:attrMapLastName": "Last Name"
     }
-	},
-
+  },
   "markdown_setting": {
     "line_break_setting": "Line Break Setting",
     "line_break_setting_desc": "You can change line break settings.",
@@ -623,14 +580,13 @@
     "Ignore all tags desc": "Stripe all HTML tags and attributes",
     "Recommended setting": "Recommended Setting",
     "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",
     "updated_lineBreak": "Succeeded to update line braek setting",
     "updated_presentation": "Succeeded to update presentation setting",
     "updated_xss": "Succeeded to update XSS setting"
   },
-
   "notification_setting": {
     "notification_list": "List of Notification Settings",
     "add_notification": "Add New",
@@ -650,7 +606,6 @@
       "ifttt_link": "Create a new IFTTT applet with Email trigger"
     }
   },
-
   "customize_page": {
     "recommended":"Recommended",
     "Behavior": "Behavior",
@@ -714,7 +669,6 @@
       "crowi_text5":"<code>/nonexistent_page/</code> the list of sub pages"
     }
   },
-
   "user_management": {
     "target_user": "Target User",
     "new_password": "New Password",
@@ -743,10 +697,10 @@
     "reset_password": "Reset Password",
     "related_username": "Related user's ",
     "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.",
     "current_users": "Current users:",
     "valid_email": "Valid email address is required",
@@ -758,8 +712,12 @@
     "remove_user_success": "Succeeded to removing {{username}} ",
     "remove_external_user_success": "Succeeded to remove {{accountId}} "
   },
-
   "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",
     "back_to_list": "Go Back to Group List",
     "basic_info": "Basic Info",
@@ -781,7 +739,6 @@
     "no_pages": "There are no pages the group has view permission",
     "remove_from_group": "Remove this user"
   },
-
   "importer_management": {
     "beta_warning": "This function is Beta.",
     "import_from": "Import from {{from}}",
@@ -840,13 +797,12 @@
     "page_skip": "Pages with a name that already exists on GROWI are not imported",
     "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": {
     "exporting_collection_list": "Exporting Collection List",

+ 23 - 67
resource/locales/ja/translation.json

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

+ 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;
 
       this.setState({
-        total,
+        total: total || 0,
         activePage: pageNum,
         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 { withTranslation } from 'react-i18next';
 
+import { AsyncTypeahead } from 'react-bootstrap-typeahead';
+import { debounce } from 'throttle-debounce';
 import { createSubscribedElement } from '../../UnstatedUtils';
 import AppContainer from '../../../services/AppContainer';
 import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
 import { toastSuccess, toastError } from '../../../util/apiNotification';
+import UserPicture from '../../User/UserPicture';
 
 class UserGroupUserFormByInput extends React.Component {
 
@@ -13,55 +16,133 @@ class UserGroupUserFormByInput extends React.Component {
     super(props);
 
     this.state = {
-      username: '',
+      keyword: '',
+      inputUser: '',
+      applicableUsers: [],
+      isLoading: false,
+      searchError: null,
     };
 
     this.xss = window.xss;
 
-    this.changeUsername = this.changeUsername.bind(this);
     this.addUserBySubmit = this.addUserBySubmit.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 {
-      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) {
-      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() {
-    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() {
     const { t } = this.props;
 
+    const inputProps = { autoComplete: 'off' };
+
     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>
-        <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 AppContainer from '../../../services/AppContainer';
 import UserGroupDetailContainer from '../../../services/UserGroupDetailContainer';
+import RadioButtonForSerchUserOption from './RadioButtonForSerchUserOption';
+import CheckBoxForSerchUserOption from './CheckBoxForSerchUserOption';
 
 class UserGroupUserModal extends React.Component {
 
@@ -16,10 +18,54 @@ class UserGroupUserModal extends React.Component {
     return (
       <Modal show={userGroupDetailContainer.state.isUserGroupUserModalOpen} onHide={userGroupDetailContainer.closeUserGroupUserModal}>
         <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.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>
     );

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

@@ -24,10 +24,15 @@ export default class UserGroupDetailContainer extends Container {
       userGroupRelations: [],
       relatedPages: [],
       isUserGroupUserModalOpen: false,
+      searchType: 'partial',
+      isAlsoMailSearched: false,
+      isAlsoNameSearched: false,
     };
 
     this.init();
 
+    this.switchIsAlsoMailSearched = this.switchIsAlsoMailSearched.bind(this);
+    this.switchIsAlsoNameSearched = this.switchIsAlsoNameSearched.bind(this);
     this.openUserGroupUserModal = this.openUserGroupUserModal.bind(this);
     this.closeUserGroupUserModal = this.closeUserGroupUserModal.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
    *
@@ -99,6 +125,24 @@ export default class UserGroupDetailContainer extends Container {
     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
    *
@@ -107,6 +151,10 @@ export default class UserGroupDetailContainer extends Container {
    */
   async addUserByUsername(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;
 
     this.setState((prevState) => {

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

@@ -194,15 +194,33 @@ class UserGroupRelation {
    * @returns {Promise<User>}
    * @memberof UserGroupRelation
    */
-  static findUserByNotRelatedGroup(userGroup) {
+  static findUserByNotRelatedGroup(userGroup, queryOptions) {
     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)
       .then((relations) => {
         const relatedUserIds = relations.map((relation) => {
           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);
         return User.find(query).exec();
@@ -213,15 +231,15 @@ class UserGroupRelation {
    * get if the user has relation for group
    *
    * @static
-   * @param {User} userData
    * @param {UserGroup} userGroup
+   * @param {User} user
    * @returns {Promise<boolean>} is user related for group(or not)
    * @memberof UserGroupRelation
    */
-  static isRelatedUserForGroup(userData, userGroup) {
+  static isRelatedUserForGroup(userGroup, user) {
     const query = {
       relatedGroup: userGroup.id,
-      relatedUser: userData.id,
+      relatedUser: user.id,
     };
 
     return this

+ 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) => {
     const { id } = req.params;
+    const {
+      searchWord, searchType, isAlsoNameSearched, isAlsoMailSearched,
+    } = req.query;
+
+    const queryOptions = {
+      searchWord, searchType, isAlsoNameSearched, isAlsoMailSearched,
+    };
 
     try {
       const userGroup = await UserGroup.findById(id);
-      const users = await UserGroupRelation.findUserByNotRelatedGroup(userGroup);
+      const users = await UserGroupRelation.findUserByNotRelatedGroup(userGroup, queryOptions);
 
       return res.apiv3({ users });
     }
@@ -381,6 +388,14 @@ module.exports = (crowi) => {
         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);
       await userGroupRelation.populate('relatedUser', User.USER_PUBLIC_FIELDS).execPopulate();