diff --git a/.vscode/settings.json b/.vscode/settings.json index db563c27c..b7d39e6e5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -2,5 +2,8 @@ "eslint.workingDirectories": [ "ui" ], - "explorer.autoReveal": "focusNoScroll" + "explorer.autoReveal": "focusNoScroll", + "cSpell.words": [ + "grecaptcha" + ] } diff --git a/i18n/af_ZA.yaml b/i18n/af_ZA.yaml index 89c34f733..8c6d0d7db 100644 --- a/i18n/af_ZA.yaml +++ b/i18n/af_ZA.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/ar_SA.yaml b/i18n/ar_SA.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/ar_SA.yaml +++ b/i18n/ar_SA.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/az_AZ.yaml b/i18n/az_AZ.yaml index 483744f70..07e505418 100644 --- a/i18n/az_AZ.yaml +++ b/i18n/az_AZ.yaml @@ -708,7 +708,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -803,7 +805,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/bal_BA.yaml b/i18n/bal_BA.yaml index 483744f70..07e505418 100644 --- a/i18n/bal_BA.yaml +++ b/i18n/bal_BA.yaml @@ -708,7 +708,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -803,7 +805,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/ban_ID.yaml b/i18n/ban_ID.yaml index 483744f70..07e505418 100644 --- a/i18n/ban_ID.yaml +++ b/i18n/ban_ID.yaml @@ -708,7 +708,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -803,7 +805,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/bn_BD.yaml b/i18n/bn_BD.yaml index 483744f70..07e505418 100644 --- a/i18n/bn_BD.yaml +++ b/i18n/bn_BD.yaml @@ -708,7 +708,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -803,7 +805,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/bs_BA.yaml b/i18n/bs_BA.yaml index 483744f70..07e505418 100644 --- a/i18n/bs_BA.yaml +++ b/i18n/bs_BA.yaml @@ -708,7 +708,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -803,7 +805,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/ca_ES.yaml b/i18n/ca_ES.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/ca_ES.yaml +++ b/i18n/ca_ES.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/cs_CZ.yaml b/i18n/cs_CZ.yaml index e71eeda48..0a260a046 100644 --- a/i18n/cs_CZ.yaml +++ b/i18n/cs_CZ.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/cy_GB.yaml b/i18n/cy_GB.yaml index a1c14f5f9..a148a9fa7 100644 --- a/i18n/cy_GB.yaml +++ b/i18n/cy_GB.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/da_DK.yaml b/i18n/da_DK.yaml index 0d48719dc..c210b1887 100644 --- a/i18n/da_DK.yaml +++ b/i18n/da_DK.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/de_DE.yaml b/i18n/de_DE.yaml index 7ac93bfd7..ed6b2ef2c 100644 --- a/i18n/de_DE.yaml +++ b/i18n/de_DE.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- Wir haben eine E-Mail an diese Adresse geschickt. Bitte befolge die Anweisungen zur Bestätigung. email: + label: E-Mail + new_email: label: Neue E-Mail msg: Neue E-Mail darf nicht leer sein. pass: diff --git a/i18n/el_GR.yaml b/i18n/el_GR.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/el_GR.yaml +++ b/i18n/el_GR.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 943608dd6..ee47b3e1d 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1029,6 +1029,8 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1213,7 +1215,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/es_ES.yaml b/i18n/es_ES.yaml index 2ffbe74c5..272b22082 100644 --- a/i18n/es_ES.yaml +++ b/i18n/es_ES.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- Te hemos enviado un email a esa dirección. Por favor sigue las instrucciones de confirmación. email: + label: Correo + new_email: label: Nuevo correo msg: El nuevo correo no puede estar vacío. pass: diff --git a/i18n/fi_FI.yaml b/i18n/fi_FI.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/fi_FI.yaml +++ b/i18n/fi_FI.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/he_IL.yaml b/i18n/he_IL.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/he_IL.yaml +++ b/i18n/he_IL.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/hi_IN.yaml b/i18n/hi_IN.yaml index 0d48719dc..c210b1887 100644 --- a/i18n/hi_IN.yaml +++ b/i18n/hi_IN.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/hu_HU.yaml b/i18n/hu_HU.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/hu_HU.yaml +++ b/i18n/hu_HU.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/hy_AM.yaml b/i18n/hy_AM.yaml index 483744f70..07e505418 100644 --- a/i18n/hy_AM.yaml +++ b/i18n/hy_AM.yaml @@ -708,7 +708,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -803,7 +805,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/id_ID.yaml b/i18n/id_ID.yaml index de2f95c43..e0278c462 100644 --- a/i18n/id_ID.yaml +++ b/i18n/id_ID.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/ja_JP.yaml b/i18n/ja_JP.yaml index d1452052f..f4a290ace 100644 --- a/i18n/ja_JP.yaml +++ b/i18n/ja_JP.yaml @@ -518,7 +518,7 @@ ui: edit_tag: タグを編集 ask_a_question: 質問を追加 edit_question: 質問を編集 - edit_answer: 回答を編集  + edit_answer: 回答を編集 search: 検索 posts_containing: Posts containing settings: 設定 @@ -763,8 +763,8 @@ ui: Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting. tip_vote: It adds something useful to the post edit_answer: - title: 回答を編集  - default_reason: 回答を編集  + title: 回答を編集 + default_reason: 回答を編集 default_first_reason: 回答を追加 form: fields: @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/ko_KR.yaml b/i18n/ko_KR.yaml index 1e584537d..d04af1b59 100644 --- a/i18n/ko_KR.yaml +++ b/i18n/ko_KR.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/nl_NL.yaml b/i18n/nl_NL.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/nl_NL.yaml +++ b/i18n/nl_NL.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/no_NO.yaml b/i18n/no_NO.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/no_NO.yaml +++ b/i18n/no_NO.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/sk_SK.yaml b/i18n/sk_SK.yaml index 7daf62b06..f3fa8dd41 100644 --- a/i18n/sk_SK.yaml +++ b/i18n/sk_SK.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- Na túto adresu sme poslali e-mail. Postupujte podľa pokynov na potvrdenie. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: diff --git a/i18n/sq_AL.yaml b/i18n/sq_AL.yaml index 483744f70..07e505418 100644 --- a/i18n/sq_AL.yaml +++ b/i18n/sq_AL.yaml @@ -708,7 +708,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -803,7 +805,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/sr_SP.yaml b/i18n/sr_SP.yaml index 28941e9e9..39b63e8ce 100644 --- a/i18n/sr_SP.yaml +++ b/i18n/sr_SP.yaml @@ -715,7 +715,9 @@ ui: We've sent an email to that address. Please follow the confirmation instructions. email: label: Email - msg: Email cannot be empty. + new_email: + label: New email + msg: New email cannot be empty. password_title: Password current_pass: label: Current Password @@ -811,7 +813,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/sv_SE.yaml b/i18n/sv_SE.yaml index 7cef69322..2030c9515 100644 --- a/i18n/sv_SE.yaml +++ b/i18n/sv_SE.yaml @@ -1176,7 +1176,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/te_IN.yaml b/i18n/te_IN.yaml index dc22340cd..bc5070233 100644 --- a/i18n/te_IN.yaml +++ b/i18n/te_IN.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/tr_TR.yaml b/i18n/tr_TR.yaml index 0460fda5c..7a9cc5918 100644 --- a/i18n/tr_TR.yaml +++ b/i18n/tr_TR.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/uk_UA.yaml b/i18n/uk_UA.yaml index 21421f664..acd8b2f84 100644 --- a/i18n/uk_UA.yaml +++ b/i18n/uk_UA.yaml @@ -1176,7 +1176,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/vi_VN.yaml b/i18n/vi_VN.yaml index d28041ce4..925a1434b 100644 --- a/i18n/vi_VN.yaml +++ b/i18n/vi_VN.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- We've sent an email to that address. Please follow the confirmation instructions. email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: @@ -1176,7 +1178,7 @@ ui: more: More tips: title: Advanced Search Tips - tag: "<1>[tag] search withing a tag" + tag: "<1>[tag] search with a tag" user: "<1>user:username search by author" answer: "<1>answers:0 unanswered questions" score: "<1>score:3 posts with a 3+ score" diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index f4d142ca4..0a6258d6c 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- 邮件已发送。请根据指引完成验证。 email: + label: 电子邮件地址 + new_email: label: 新的电子邮件地址 msg: 新邮箱不能为空。 pass: diff --git a/i18n/zh_TW.yaml b/i18n/zh_TW.yaml index c0377af7c..4b5cce48e 100644 --- a/i18n/zh_TW.yaml +++ b/i18n/zh_TW.yaml @@ -999,6 +999,8 @@ ui: change_email_info: >- 我們已經寄出一封郵件至此電子郵件地址,請遵照說明進行確認。 email: + label: Email + new_email: label: New email msg: New email cannot be empty. pass: diff --git a/ui/src/components/Actions/index.tsx b/ui/src/components/Actions/index.tsx index b83571054..b58d0c8cd 100644 --- a/ui/src/components/Actions/index.tsx +++ b/ui/src/components/Actions/index.tsx @@ -25,7 +25,8 @@ import classNames from 'classnames'; import { Icon } from '@/components'; import { loggedUserInfoStore } from '@/stores'; -import { useToast, useCaptchaModal } from '@/hooks'; +import { useToast } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { tryNormalLogged } from '@/utils/guard'; import { bookmark, postVote } from '@/services'; import * as Types from '@/common/interface'; @@ -56,7 +57,7 @@ const Index: FC = ({ className, data, source }) => { const { username = '' } = loggedUserInfoStore((state) => state.user); const toast = useToast(); const { t } = useTranslation(); - const vCaptcha = useCaptchaModal('vote'); + const vCaptcha = useCaptchaPlugin('vote'); useEffect(() => { if (data) { @@ -70,6 +71,42 @@ const Index: FC = ({ className, data, source }) => { } }, []); + const submitVote = (type) => { + const isCancel = (type === 'up' && like) || (type === 'down' && hate); + const imgCode: Types.ImgCodeReq = { + captcha_id: undefined, + captcha_code: undefined, + }; + vCaptcha?.resolveCaptchaReq?.(imgCode); + + postVote( + { + object_id: data?.id, + is_cancel: isCancel, + ...imgCode, + }, + type, + ) + .then(async (res) => { + await vCaptcha?.close(); + setVotes(res.votes); + setLike(res.vote_status === 'vote_up'); + setHated(res.vote_status === 'vote_down'); + }) + .catch((err) => { + if (err?.isError) { + vCaptcha?.handleCaptchaError(err.list); + } + const errMsg = err?.value; + if (errMsg) { + toast.onShow({ + msg: errMsg, + variant: 'danger', + }); + } + }); + }; + const handleVote = (type: 'up' | 'down') => { if (!tryNormalLogged(true)) { return; @@ -82,39 +119,14 @@ const Index: FC = ({ className, data, source }) => { }); return; } - const isCancel = (type === 'up' && like) || (type === 'down' && hate); + + if (!vCaptcha) { + submitVote(type); + return; + } + vCaptcha.check(() => { - const imgCode: Types.ImgCodeReq = { - captcha_id: undefined, - captcha_code: undefined, - }; - vCaptcha.resolveCaptchaReq(imgCode); - postVote( - { - object_id: data?.id, - is_cancel: isCancel, - ...imgCode, - }, - type, - ) - .then(async (res) => { - await vCaptcha.close(); - setVotes(res.votes); - setLike(res.vote_status === 'vote_up'); - setHated(res.vote_status === 'vote_down'); - }) - .catch((err) => { - if (err?.isError) { - vCaptcha.handleCaptchaError(err.list); - } - const errMsg = err?.value; - if (errMsg) { - toast.onShow({ - msg: errMsg, - variant: 'danger', - }); - } - }); + submitVote(type); }); }; diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index 81345e7e7..ca69b2dee 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -35,6 +35,7 @@ import { bgFadeOut, } from '@/utils'; import { tryNormalLogged } from '@/utils/guard'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { useQueryComments, addComment, @@ -66,9 +67,9 @@ const Comment = ({ objectId, mode, commentId }) => { const reportModal = useReportModal(); const addCaptcha = useCaptchaModal('comment'); - const editCaptcha = useCaptchaModal('edit'); - const dCaptcha = useCaptchaModal('delete'); - const vCaptcha = useCaptchaModal('vote'); + const editCaptcha = useCaptchaPlugin('edit'); + const dCaptcha = useCaptchaPlugin('delete'); + const vCaptcha = useCaptchaPlugin('vote'); const { t } = useTranslation('translation', { keyPrefix: 'comment' }); @@ -136,6 +137,85 @@ const Comment = ({ objectId, mode, commentId }) => { ); }; + const submitUpdateComment = (params, item) => { + const up = { + ...params, + comment_id: item.comment_id, + captcha_code: undefined, + captcha_id: undefined, + }; + editCaptcha?.resolveCaptchaReq(up); + + return updateComment(up) + .then(async (res) => { + await editCaptcha?.close(); + setComments( + comments.map((comment) => { + if (comment.comment_id === item.comment_id) { + comment.showEdit = false; + comment.parsed_text = res.parsed_text; + comment.original_text = res.original_text; + } + return comment; + }), + ); + }) + .catch((err) => { + if (err.isError) { + const captchaErr = editCaptcha?.handleCaptchaError(err.list); + // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing. + if (!(captchaErr && err.list.length === 1)) { + return Promise.reject(err); + } + } + return Promise.resolve(); + }); + }; + + const submitAddComment = (params, item) => { + const req = { + ...params, + captcha_code: undefined, + captcha_id: undefined, + }; + addCaptcha?.resolveCaptchaReq(req); + + return addComment(req) + .then(async (res) => { + await addCaptcha?.close(); + if (item.type === 'reply') { + const index = comments.findIndex( + (comment) => comment.comment_id === item.comment_id, + ); + updateCurrentReplyId(''); + comments.splice(index + 1, 0, res); + setComments([...comments]); + } else { + setComments([ + ...comments.map((comment) => { + if (comment.comment_id === item.comment_id) { + updateCurrentReplyId(''); + } + return comment; + }), + res, + ]); + } + + setVisibleComment(false); + }) + .catch((ex) => { + if (ex.isError) { + const captchaErr = addCaptcha?.handleCaptchaError(ex.list); + // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing. + if (!(captchaErr && ex.list.length === 1)) { + return Promise.reject(ex); + } + } + return Promise.resolve(); + }); + }; + const handleSendReply = (item) => { const users = matchedUsers(item.value); const userNames = unionBy(users.map((user) => user.userName)); @@ -153,80 +233,36 @@ const Comment = ({ objectId, mode, commentId }) => { }; if (item.type === 'edit') { - return editCaptcha.check(() => { - const up = { - ...params, - comment_id: item.comment_id, - captcha_code: undefined, - captcha_id: undefined, - }; - editCaptcha.resolveCaptchaReq(up); - - return updateComment(up) - .then(async (res) => { - await editCaptcha.close(); - setComments( - comments.map((comment) => { - if (comment.comment_id === item.comment_id) { - comment.showEdit = false; - comment.parsed_text = res.parsed_text; - comment.original_text = res.original_text; - } - return comment; - }), - ); - }) - .catch((err) => { - if (err.isError) { - editCaptcha.handleCaptchaError(err.list); - } - }); - }); + if (!editCaptcha) { + return submitUpdateComment(params, item); + } + return editCaptcha.check(() => submitUpdateComment(params, item)); } - return addCaptcha.check(() => { - const req = { - ...params, - captcha_code: undefined, - captcha_id: undefined, - }; - addCaptcha.resolveCaptchaReq(req); - - return addComment(req) - .then(async (res) => { - await addCaptcha.close(); - if (item.type === 'reply') { - const index = comments.findIndex( - (comment) => comment.comment_id === item.comment_id, - ); - updateCurrentReplyId(''); - comments.splice(index + 1, 0, res); - setComments([...comments]); - } else { - setComments([ - ...comments.map((comment) => { - if (comment.comment_id === item.comment_id) { - updateCurrentReplyId(''); - } - return comment; - }), - res, - ]); - } + if (!addCaptcha) { + return submitAddComment(params, item); + } - setVisibleComment(false); - }) - .catch((ex) => { - if (ex.isError) { - const captchaErr = addCaptcha.handleCaptchaError(ex.list); - // If it is not a CAPTCHA error, leave it to the subsequent error handling logic to continue processing. - if (!(captchaErr && ex.list.length === 1)) { - return Promise.reject(ex); - } - } - return Promise.resolve(); - }); - }); + return addCaptcha.check(() => submitAddComment(params, item)); + }; + + const submitDeleteComment = (id) => { + const imgCode = { captcha_id: undefined, captcha_code: undefined }; + dCaptcha?.resolveCaptchaReq(imgCode); + + deleteComment(id, imgCode) + .then(async () => { + await dCaptcha?.close(); + if (pageIndex === 0) { + mutate(); + } + setComments(comments.filter((item) => item.comment_id !== id)); + }) + .catch((ex) => { + if (ex.isError) { + dCaptcha?.handleCaptchaError(ex.list); + } + }); }; const handleDelete = (id) => { @@ -236,67 +272,64 @@ const Comment = ({ objectId, mode, commentId }) => { confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), onConfirm: () => { + if (!dCaptcha) { + submitDeleteComment(id); + return; + } dCaptcha.check(() => { - const imgCode = { captcha_id: undefined, captcha_code: undefined }; - dCaptcha.resolveCaptchaReq(imgCode); - - deleteComment(id, imgCode) - .then(async () => { - await dCaptcha.close(); - if (pageIndex === 0) { - mutate(); - } - setComments(comments.filter((item) => item.comment_id !== id)); - }) - .catch((ex) => { - if (ex.isError) { - dCaptcha.handleCaptchaError(ex.list); - } - }); + submitDeleteComment(id); }); }, }); }; + const submitVoteComment = (id, is_cancel) => { + const imgCode: Types.ImgCodeReq = { + captcha_id: undefined, + captcha_code: undefined, + }; + vCaptcha?.resolveCaptchaReq(imgCode); + + postVote( + { + object_id: id, + is_cancel, + ...imgCode, + }, + 'up', + ) + .then(async () => { + await vCaptcha?.close(); + setComments( + comments.map((item) => { + if (item.comment_id === id) { + item.vote_count = is_cancel + ? item.vote_count - 1 + : item.vote_count + 1; + item.is_vote = !is_cancel; + } + return item; + }), + ); + }) + .catch((ex) => { + if (ex.isError) { + vCaptcha?.handleCaptchaError(ex.list); + } + }); + }; const handleVote = (id, is_cancel) => { if (!tryNormalLogged(true)) { return; } + if (!vCaptcha) { + submitVoteComment(id, is_cancel); + return; + } + vCaptcha.check(() => { - const imgCode: Types.ImgCodeReq = { - captcha_id: undefined, - captcha_code: undefined, - }; - vCaptcha.resolveCaptchaReq(imgCode); - - postVote( - { - object_id: id, - is_cancel, - ...imgCode, - }, - 'up', - ) - .then(async () => { - await vCaptcha.close(); - setComments( - comments.map((item) => { - if (item.comment_id === id) { - item.vote_count = is_cancel - ? item.vote_count - 1 - : item.vote_count + 1; - item.is_vote = !is_cancel; - } - return item; - }), - ); - }) - .catch((ex) => { - if (ex.isError) { - vCaptcha.handleCaptchaError(ex.list); - } - }); + submitVoteComment(id, is_cancel); }); }; diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index e04416f2f..47b4463f0 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -23,7 +23,8 @@ import { Link, useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { Modal } from '@/components'; -import { useReportModal, useToast, useCaptchaModal } from '@/hooks'; +import { useReportModal, useToast } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { QuestionOperationReq } from '@/common/interface'; import Share from '../Share'; import { @@ -63,7 +64,7 @@ const Index: FC = ({ const toast = useToast(); const navigate = useNavigate(); const reportModal = useReportModal(); - const dCaptcha = useCaptchaModal('delete'); + const dCaptcha = useCaptchaPlugin('delete'); const refreshQuestion = () => { callback?.('default'); @@ -88,6 +89,55 @@ const Index: FC = ({ }); }; + const submitDeleteQuestion = () => { + const req = { + id: qid, + captcha_code: undefined, + captcha_id: undefined, + }; + dCaptcha?.resolveCaptchaReq(req); + + deleteQuestion(req) + .then(async () => { + await dCaptcha?.close(); + toast.onShow({ + msg: t('post_deleted', { keyPrefix: 'messages' }), + variant: 'success', + }); + callback?.('delete_question'); + }) + .catch((ex) => { + if (ex.isError) { + dCaptcha?.handleCaptchaError(ex.list); + } + }); + }; + + const submitDeleteAnswer = () => { + const req = { + id: aid, + captcha_code: undefined, + captcha_id: undefined, + }; + dCaptcha?.resolveCaptchaReq(req); + + deleteAnswer(req) + .then(async () => { + await dCaptcha?.close(); + // refresh page + toast.onShow({ + msg: t('tip_answer_deleted'), + variant: 'success', + }); + callback?.('delete_answer'); + }) + .catch((ex) => { + if (ex.isError) { + dCaptcha?.handleCaptchaError(ex.list); + } + }); + }; + const handleDelete = () => { if (type === 'question') { Modal.confirm({ @@ -97,28 +147,12 @@ const Index: FC = ({ confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), onConfirm: () => { + if (!dCaptcha) { + submitDeleteQuestion(); + return; + } dCaptcha.check(() => { - const req = { - id: qid, - captcha_code: undefined, - captcha_id: undefined, - }; - dCaptcha.resolveCaptchaReq(req); - - deleteQuestion(req) - .then(async () => { - await dCaptcha.close(); - toast.onShow({ - msg: t('post_deleted', { keyPrefix: 'messages' }), - variant: 'success', - }); - callback?.('delete_question'); - }) - .catch((ex) => { - if (ex.isError) { - dCaptcha.handleCaptchaError(ex.list); - } - }); + submitDeleteQuestion(); }); }, }); @@ -132,29 +166,12 @@ const Index: FC = ({ confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), onConfirm: () => { + if (!dCaptcha) { + submitDeleteAnswer(); + return; + } dCaptcha.check(() => { - const req = { - id: aid, - captcha_code: undefined, - captcha_id: undefined, - }; - dCaptcha.resolveCaptchaReq(req); - - deleteAnswer(req) - .then(async () => { - await dCaptcha.close(); - // refresh page - toast.onShow({ - msg: t('tip_answer_deleted'), - variant: 'success', - }); - callback?.('delete_answer'); - }) - .catch((ex) => { - if (ex.isError) { - dCaptcha.handleCaptchaError(ex.list); - } - }); + submitDeleteAnswer(); }); }, }); diff --git a/ui/src/components/PluginRender/index.tsx b/ui/src/components/PluginRender/index.tsx index 3da9fd0b3..2426eb9cd 100644 --- a/ui/src/components/PluginRender/index.tsx +++ b/ui/src/components/PluginRender/index.tsx @@ -19,7 +19,8 @@ import React, { FC, ReactNode } from 'react'; -import PluginKit, { Plugin, PluginType } from '@/utils/pluginKit'; +import PluginKit, { Plugin } from '@/utils/pluginKit'; +import type { PluginType } from '@/utils/pluginKit/interface'; /** * Note:Please set at least either of the `slug_name` and `type` attributes, otherwise no plugins will be rendered. * diff --git a/ui/src/components/SideNav/index.scss b/ui/src/components/SideNav/index.scss index d8bcc8f11..0cf6201bc 100644 --- a/ui/src/components/SideNav/index.scss +++ b/ui/src/components/SideNav/index.scss @@ -48,7 +48,6 @@ width: 1px; height: 100%; background-color: var(--bs-border-color); - min-height: calc(100vh - 62px - 74px); } } diff --git a/ui/src/components/Unactivate/index.tsx b/ui/src/components/Unactivate/index.tsx index e96e28754..163da2bd3 100644 --- a/ui/src/components/Unactivate/index.tsx +++ b/ui/src/components/Unactivate/index.tsx @@ -26,7 +26,7 @@ import type { ImgCodeReq, FormDataType } from '@/common/interface'; import { loggedUserInfoStore } from '@/stores'; import { resendEmail } from '@/services'; import { handleFormError } from '@/utils'; -import { useCaptchaModal } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; interface IProps { visible?: boolean; @@ -44,12 +44,12 @@ const Index: React.FC = () => { }, }); - const emailCaptcha = useCaptchaModal('email'); + const emailCaptcha = useCaptchaPlugin('email'); const submit = () => { let req: ImgCodeReq = {}; - const imgCode = emailCaptcha.getCaptcha(); - if (imgCode.verify) { + const imgCode = emailCaptcha?.getCaptcha(); + if (imgCode?.verify) { req = { captcha_code: imgCode.captcha_code, captcha_id: imgCode.captcha_id, @@ -57,12 +57,12 @@ const Index: React.FC = () => { } resendEmail(req) .then(async () => { - await emailCaptcha.close(); + await emailCaptcha?.close(); setSuccess(true); }) .catch((err) => { if (err.isError) { - emailCaptcha.handleCaptchaError(err.list); + emailCaptcha?.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); } @@ -71,6 +71,10 @@ const Index: React.FC = () => { const onSentEmail = (evt) => { evt.preventDefault(); + if (!emailCaptcha) { + submit(); + return; + } emailCaptcha.check(() => { submit(); }); diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index e20b8eb61..843472cd7 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -23,7 +23,8 @@ import { useTranslation } from 'react-i18next'; import ReactDOM from 'react-dom/client'; -import { useToast, useCaptchaModal } from '@/hooks'; +import { useToast } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import type * as Type from '@/common/interface'; import { reportList, @@ -65,7 +66,7 @@ const useReportModal = (callback?: () => void) => { const [show, setShow] = useState(false); const [list, setList] = useState([]); - const rCaptcha = useCaptchaModal('report'); + const rCaptcha = useCaptchaPlugin('report'); useEffect(() => { const div = document.createElement('div'); @@ -168,6 +169,34 @@ const useReportModal = (callback?: () => void) => { return true; }; + const submitReport = (data) => { + const flagReq = { + source: data.type, + report_type: reportType.type, + object_id: data.id, + content: content.value, + captcha_code: undefined, + captcha_id: undefined, + }; + rCaptcha?.resolveCaptchaReq(flagReq); + + postReport(flagReq) + .then(async () => { + await rCaptcha?.close(); + toast.onShow({ + msg: t('flag_success', { keyPrefix: 'toast' }), + variant: 'warning', + }); + onClose(); + asyncCallback(); + }) + .catch((ex) => { + if (ex.isError) { + rCaptcha?.handleCaptchaError(ex.list); + } + }); + }; + const handleSubmit = () => { if (!params) { return; @@ -205,32 +234,12 @@ const useReportModal = (callback?: () => void) => { return; } if (!params.isBackend && params.action === 'flag') { + if (!rCaptcha) { + submitReport(params); + return; + } rCaptcha.check(() => { - const flagReq = { - source: params.type, - report_type: reportType.type, - object_id: params.id, - content: content.value, - captcha_code: undefined, - captcha_id: undefined, - }; - rCaptcha.resolveCaptchaReq(flagReq); - - postReport(flagReq) - .then(async () => { - await rCaptcha.close(); - toast.onShow({ - msg: t('flag_success', { keyPrefix: 'toast' }), - variant: 'warning', - }); - onClose(); - asyncCallback(); - }) - .catch((ex) => { - if (ex.isError) { - rCaptcha.handleCaptchaError(ex.list); - } - }); + submitReport(params); }); } diff --git a/ui/src/pages/Layout/index.tsx b/ui/src/pages/Layout/index.tsx index f0a061bd0..cf7a2f1ba 100644 --- a/ui/src/pages/Layout/index.tsx +++ b/ui/src/pages/Layout/index.tsx @@ -75,7 +75,7 @@ const Layout: FC = () => { }}>
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */} -
+
{httpStatusCode ? ( ) : ( diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index 9a1ff14a9..367ccfca8 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -27,7 +27,7 @@ import classNames from 'classnames'; import isEqual from 'lodash/isEqual'; import debounce from 'lodash/debounce'; -import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks'; +import { usePageTags, usePromptWithUnload } from '@/hooks'; import { Editor, EditorRef, TagSelector } from '@/components'; import type * as Type from '@/common/interface'; import { DRAFT_QUESTION_STORAGE_KEY } from '@/common/constants'; @@ -42,6 +42,7 @@ import { } from '@/services'; import { handleFormError, SaveDraft, storageExpires } from '@/utils'; import { pathFactory } from '@/router/pathFactory'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import SearchQuestion from './components/SearchQuestion'; @@ -120,8 +121,8 @@ const Ask = () => { const isEdit = qid !== undefined; - const saveCaptcha = useCaptchaModal('question'); - const editCaptcha = useCaptchaModal('edit'); + const saveCaptcha = useCaptchaPlugin('question'); + const editCaptcha = useCaptchaPlugin('edit'); const removeDraft = () => { saveDraft.save.cancel(); @@ -276,6 +277,79 @@ const Ask = () => { } }; + const submitModifyQuestion = (params) => { + contentChangedRef.current = false; + const ep = { + ...params, + id: qid, + edit_summary: formData.edit_summary.value, + }; + const imgCode = editCaptcha?.getCaptcha(); + if (imgCode?.verify) { + ep.captcha_code = imgCode.captcha_code; + ep.captcha_id = imgCode.captcha_id; + } + modifyQuestion(ep) + .then(async (res) => { + await editCaptcha?.close(); + navigate(pathFactory.questionLanding(qid, res?.url_title), { + state: { isReview: res?.wait_for_review }, + }); + }) + .catch((err) => { + if (err.isError) { + editCaptcha?.handleCaptchaError(err.list); + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + }; + + const submitQuestion = async (params) => { + contentChangedRef.current = false; + const imgCode = saveCaptcha?.getCaptcha(); + if (imgCode?.verify) { + params.captcha_code = imgCode.captcha_code; + params.captcha_id = imgCode.captcha_id; + } + let res; + if (checked) { + res = await saveQuestionWithAnswer({ + ...params, + answer_content: formData.answer_content.value, + }).catch((err) => { + if (err.isError) { + const captchaErr = saveCaptcha?.handleCaptchaError(err.list); + if (!(captchaErr && err.list.length === 1)) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + } + }); + } else { + res = await saveQuestion(params).catch((err) => { + if (err.isError) { + const captchaErr = saveCaptcha?.handleCaptchaError(err.list); + if (!(captchaErr && err.list.length === 1)) { + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + } + }); + } + + const id = res?.id || res?.question?.id; + if (id) { + await saveCaptcha?.close(); + if (checked) { + navigate(pathFactory.questionLanding(id, res?.question?.url_title)); + } else { + navigate(pathFactory.questionLanding(id, res?.url_title)); + } + } + removeDraft(); + }; + const handleSubmit = async (event: React.FormEvent) => { event.preventDefault(); event.stopPropagation(); @@ -287,77 +361,20 @@ const Ask = () => { }; if (isEdit) { + if (!editCaptcha) { + submitModifyQuestion(params); + return; + } editCaptcha.check(() => { - contentChangedRef.current = false; - const ep = { - ...params, - id: qid, - edit_summary: formData.edit_summary.value, - }; - const imgCode = editCaptcha.getCaptcha(); - if (imgCode.verify) { - ep.captcha_code = imgCode.captcha_code; - ep.captcha_id = imgCode.captcha_id; - } - modifyQuestion(ep) - .then(async (res) => { - await editCaptcha.close(); - navigate(pathFactory.questionLanding(qid, res?.url_title), { - state: { isReview: res?.wait_for_review }, - }); - }) - .catch((err) => { - if (err.isError) { - editCaptcha.handleCaptchaError(err.list); - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - }); + submitModifyQuestion(params); }); } else { - saveCaptcha.check(async () => { - contentChangedRef.current = false; - const imgCode = saveCaptcha.getCaptcha(); - if (imgCode.verify) { - params.captcha_code = imgCode.captcha_code; - params.captcha_id = imgCode.captcha_id; - } - let res; - if (checked) { - res = await saveQuestionWithAnswer({ - ...params, - answer_content: formData.answer_content.value, - }).catch((err) => { - if (err.isError) { - const captchaErr = saveCaptcha.handleCaptchaError(err.list); - if (!(captchaErr && err.list.length === 1)) { - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - } - }); - } else { - res = await saveQuestion(params).catch((err) => { - if (err.isError) { - const captchaErr = saveCaptcha.handleCaptchaError(err.list); - if (!(captchaErr && err.list.length === 1)) { - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - } - }); - } - - const id = res?.id || res?.question?.id; - if (id) { - await saveCaptcha.close(); - if (checked) { - navigate(pathFactory.questionLanding(id, res?.question?.url_title)); - } else { - navigate(pathFactory.questionLanding(id, res?.url_title)); - } - } - removeDraft(); + if (!saveCaptcha) { + submitQuestion(params); + return; + } + saveCaptcha?.check(async () => { + submitQuestion(params); }); } }; diff --git a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx index 82f9fa275..b8e29942c 100644 --- a/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/InviteToAnswer/index.tsx @@ -27,7 +27,7 @@ import classNames from 'classnames'; import { Avatar } from '@/components'; import { getInviteUser, putInviteUser } from '@/services'; import type * as Type from '@/common/interface'; -import { useCaptchaModal } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import PeopleDropdown from './PeopleDropdown'; @@ -44,7 +44,7 @@ const Index: FC = ({ questionId, readOnly = false }) => { const [editing, setEditing] = useState(false); const [users, setUsers] = useState([]); - const iaCaptcha = useCaptchaModal('invitation_answer'); + const iaCaptcha = useCaptchaPlugin('invitation_answer'); const initInviteUsers = () => { if (!questionId) { @@ -74,28 +74,35 @@ const Index: FC = ({ questionId, readOnly = false }) => { setUsers(userList); }; + const submitInviteUser = () => { + const names = users.map((_) => { + return _.username; + }); + const imgCode: Type.ImgCodeReq = {}; + iaCaptcha?.resolveCaptchaReq(imgCode); + putInviteUser(questionId, names, imgCode) + .then(async () => { + await iaCaptcha?.close(); + setEditing(false); + }) + .catch((ex) => { + if (ex.isError) { + iaCaptcha?.handleCaptchaError(ex.list); + } + }); + }; + const saveInviteUsers = () => { if (!users) { return; } - const names = users.map((_) => { - return _.username; - }); - iaCaptcha.check(() => { - const imgCode: Type.ImgCodeReq = {}; - iaCaptcha.resolveCaptchaReq(imgCode); - putInviteUser(questionId, names, imgCode) - .then(async () => { - await iaCaptcha.close(); - setEditing(false); - }) - .catch((ex) => { - if (ex.isError) { - iaCaptcha.handleCaptchaError(ex.list); - } - console.error('putInviteUser error: ', ex); - }); - }); + + if (!iaCaptcha) { + submitInviteUser(); + return; + } + + iaCaptcha.check(() => submitInviteUser()); }; useEffect(() => { diff --git a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx index 9bc8be448..456fbe370 100644 --- a/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/WriteAnswer/index.tsx @@ -25,7 +25,8 @@ import { Link } from 'react-router-dom'; import { marked } from 'marked'; import classNames from 'classnames'; -import { usePromptWithUnload, useCaptchaModal } from '@/hooks'; +import { usePromptWithUnload } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { Editor, Modal, TextArea } from '@/components'; import { FormDataType, PostAnswerReq } from '@/common/interface'; import { postAnswer } from '@/services'; @@ -63,8 +64,9 @@ const Index: FC = ({ visible = false, data, callback }) => { const [editorFocusState, setEditorFocusState] = useState(false); const [hasDraft, setHasDraft] = useState(false); const [showTips, setShowTips] = useState(data.loggedUserRank < 100); - const aCaptcha = useCaptchaModal('answer'); + const aCaptcha = useCaptchaPlugin('answer'); const writeInfo = writeSettingStore((state) => state.write); + const [editorCanSave, setEditorCanSave] = useState(false); usePromptWithUnload({ when: Boolean(formData.content.value), @@ -80,6 +82,7 @@ const Index: FC = ({ visible = false, data, callback }) => { useEffect(() => { const draft = storageExpires.get(DRAFT_ANSWER_STORAGE_KEY); if (draft?.questionId === data.qid && draft?.content) { + setShowEditor(true); setFormData({ content: { value: draft.content, @@ -87,9 +90,11 @@ const Index: FC = ({ visible = false, data, callback }) => { errorMsg: '', }, }); - setShowEditor(true); setHasDraft(true); } + setTimeout(() => { + setEditorCanSave(true); + }, 100); }, []); useEffect(() => { @@ -152,6 +157,40 @@ const Index: FC = ({ visible = false, data, callback }) => { } }; + const submitAnswer = () => { + const params: PostAnswerReq = { + question_id: data?.qid, + content: formData.content.value, + html: marked.parse(formData.content.value), + }; + const imgCode = aCaptcha?.getCaptcha(); + if (imgCode?.verify) { + params.captcha_code = imgCode.captcha_code; + params.captcha_id = imgCode.captcha_id; + } + postAnswer(params) + .then(async (res) => { + await aCaptcha?.close(); + setShowEditor(false); + setFormData({ + content: { + value: '', + isInvalid: false, + errorMsg: '', + }, + }); + removeDraft(); + callback?.(res.info); + }) + .catch((ex) => { + if (ex.isError) { + aCaptcha?.handleCaptchaError(ex.list); + const stateData = handleFormError(ex, formData); + setFormData({ ...stateData }); + } + }); + }; + const handleSubmit = () => { if (!guard.tryNormalLogged(true)) { return; @@ -159,40 +198,11 @@ const Index: FC = ({ visible = false, data, callback }) => { if (!checkValidated()) { return; } - - aCaptcha.check(() => { - const params: PostAnswerReq = { - question_id: data?.qid, - content: formData.content.value, - html: marked.parse(formData.content.value), - }; - const imgCode = aCaptcha.getCaptcha(); - if (imgCode.verify) { - params.captcha_code = imgCode.captcha_code; - params.captcha_id = imgCode.captcha_id; - } - postAnswer(params) - .then(async (res) => { - await aCaptcha.close(); - setShowEditor(false); - setFormData({ - content: { - value: '', - isInvalid: false, - errorMsg: '', - }, - }); - removeDraft(); - callback?.(res.info); - }) - .catch((ex) => { - if (ex.isError) { - aCaptcha.handleCaptchaError(ex.list); - const stateData = handleFormError(ex, formData); - setFormData({ ...stateData }); - } - }); - }); + if (!aCaptcha) { + submitAnswer(); + return; + } + aCaptcha.check(() => submitAnswer()); }; const clickBtn = () => { @@ -260,13 +270,15 @@ const Index: FC = ({ visible = false, data, callback }) => { value={formData.content.value} autoFocus={editorFocusState} onChange={(val) => { - setFormData({ - content: { - value: val, - isInvalid: false, - errorMsg: '', - }, - }); + if (editorCanSave) { + setFormData({ + content: { + value: val, + isInvalid: false, + errorMsg: '', + }, + }); + } }} onFocus={() => { setFocusType('answer'); diff --git a/ui/src/pages/Questions/EditAnswer/index.tsx b/ui/src/pages/Questions/EditAnswer/index.tsx index fbb3e3279..49e7b33a1 100644 --- a/ui/src/pages/Questions/EditAnswer/index.tsx +++ b/ui/src/pages/Questions/EditAnswer/index.tsx @@ -26,7 +26,8 @@ import dayjs from 'dayjs'; import classNames from 'classnames'; import { handleFormError, scrollToDocTop } from '@/utils'; -import { usePageTags, usePromptWithUnload, useCaptchaModal } from '@/hooks'; +import { usePageTags, usePromptWithUnload } from '@/hooks'; +import { useCaptchaPlugin, useRenderHtmlPlugin } from '@/utils/pluginKit'; import { pathFactory } from '@/router/pathFactory'; import { Editor, EditorRef, Icon, htmlRender } from '@/components'; import type * as Type from '@/common/interface'; @@ -35,7 +36,6 @@ import { modifyAnswer, useQueryRevisions, } from '@/services'; -import { useRenderHtmlPlugin } from '@/utils/pluginKit'; import './index.scss'; @@ -71,7 +71,7 @@ const Index = () => { const [formData, setFormData] = useState(initFormData); const [immData, setImmData] = useState(initFormData); const [contentChanged, setContentChanged] = useState(false); - const editCaptcha = useCaptchaModal('edit'); + const editCaptcha = useCaptchaPlugin('edit'); useEffect(() => { if (data?.info?.content) { @@ -153,6 +153,39 @@ const Index = () => { return bol; }; + const submitEditAnswer = () => { + const params: Type.AnswerParams = { + content: formData.content.value, + html: editorRef.current.getHtml(), + question_id: qid, + id: aid, + edit_summary: formData.description.value, + }; + editCaptcha?.resolveCaptchaReq(params); + + modifyAnswer(params) + .then(async (res) => { + await editCaptcha?.close(); + navigate( + pathFactory.answerLanding({ + questionId: qid, + slugTitle: data?.question?.url_title, + answerId: aid, + }), + { + state: { isReview: res?.wait_for_review }, + }, + ); + }) + .catch((ex) => { + if (ex.isError) { + editCaptcha?.handleCaptchaError(ex.list); + const stateData = handleFormError(ex, formData); + setFormData({ ...stateData }); + } + }); + }; + const handleSubmit = async (event: React.FormEvent) => { setContentChanged(false); @@ -163,38 +196,11 @@ const Index = () => { return; } - editCaptcha.check(() => { - const params: Type.AnswerParams = { - content: formData.content.value, - html: editorRef.current.getHtml(), - question_id: qid, - id: aid, - edit_summary: formData.description.value, - }; - editCaptcha.resolveCaptchaReq(params); - - modifyAnswer(params) - .then(async (res) => { - await editCaptcha.close(); - navigate( - pathFactory.answerLanding({ - questionId: qid, - slugTitle: data?.question?.url_title, - answerId: aid, - }), - { - state: { isReview: res?.wait_for_review }, - }, - ); - }) - .catch((ex) => { - if (ex.isError) { - editCaptcha.handleCaptchaError(ex.list); - const stateData = handleFormError(ex, formData); - setFormData({ ...stateData }); - } - }); - }); + if (!editCaptcha) { + submitEditAnswer(); + return; + } + editCaptcha.check(() => submitEditAnswer()); }; const handleSelectedRevision = (e) => { const index = e.target.value; diff --git a/ui/src/pages/Review/components/ApproveDropdown/index.tsx b/ui/src/pages/Review/components/ApproveDropdown/index.tsx index f6a624716..b2c05c96a 100644 --- a/ui/src/pages/Review/components/ApproveDropdown/index.tsx +++ b/ui/src/pages/Review/components/ApproveDropdown/index.tsx @@ -23,7 +23,8 @@ import { useTranslation } from 'react-i18next'; import { Modal } from '@/components'; import { putFlagReviewAction } from '@/services'; -import { useCaptchaModal, useReportModal, useToast } from '@/hooks'; +import { useReportModal, useToast } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import type * as Type from '@/common/interface'; import EditPostModal from '../EditPostModal'; @@ -46,12 +47,52 @@ const Index: FC = ({ const [showEditPostModal, setShowEditPostModal] = useState(false); const closeModal = useReportModal(approveCallback); const toast = useToast(); - const dCaptcha = useCaptchaModal('delete'); + const dCaptcha = useCaptchaPlugin('delete'); const handleEditPostModalState = () => { setShowEditPostModal(!showEditPostModal); }; + const submitReviewAction = () => { + const req: Type.PutFlagReviewParams = { + operation_type: 'delete_post', + flag_id: String(itemData?.flag_id), + captcha_code: undefined, + captcha_id: undefined, + }; + dCaptcha?.resolveCaptchaReq(req); + + delete req.captcha_code; + delete req.captcha_id; + + putFlagReviewAction(req) + .then(async () => { + await dCaptcha?.close(); + let msg = ''; + if (objectType === 'question') { + msg = t('post_deleted', { keyPrefix: 'messages' }); + } + if (objectType === 'answer') { + msg = t('tip_answer_deleted'); + } + if (objectType === 'answer' || objectType === 'question') { + toast.onShow({ + msg, + variant: 'success', + }); + } + approveCallback(); + }) + .catch((ex) => { + if (ex.isError) { + dCaptcha?.handleCaptchaError(ex.list); + } + }) + .finally(() => { + setIsLoading(false); + }); + }; + const handleDelete = () => { let content = ''; @@ -78,45 +119,11 @@ const Index: FC = ({ confirmBtnVariant: 'danger', confirmText: t('delete', { keyPrefix: 'btns' }), onConfirm: () => { - dCaptcha.check(() => { - const req: Type.PutFlagReviewParams = { - operation_type: 'delete_post', - flag_id: String(itemData?.flag_id), - captcha_code: undefined, - captcha_id: undefined, - }; - dCaptcha.resolveCaptchaReq(req); - - delete req.captcha_code; - delete req.captcha_id; - - putFlagReviewAction(req) - .then(async () => { - await dCaptcha.close(); - let msg = ''; - if (objectType === 'question') { - msg = t('post_deleted', { keyPrefix: 'messages' }); - } - if (objectType === 'answer') { - msg = t('tip_answer_deleted'); - } - if (objectType === 'answer' || objectType === 'question') { - toast.onShow({ - msg, - variant: 'success', - }); - } - approveCallback(); - }) - .catch((ex) => { - if (ex.isError) { - dCaptcha.handleCaptchaError(ex.list); - } - }) - .finally(() => { - setIsLoading(false); - }); - }); + if (!dCaptcha) { + submitReviewAction(); + return; + } + dCaptcha.check(() => submitReviewAction()); }, onCancel: () => { setIsLoading(false); diff --git a/ui/src/pages/Review/components/EditPostModal/index.tsx b/ui/src/pages/Review/components/EditPostModal/index.tsx index 0eefd311d..4ee80b0aa 100644 --- a/ui/src/pages/Review/components/EditPostModal/index.tsx +++ b/ui/src/pages/Review/components/EditPostModal/index.tsx @@ -24,7 +24,8 @@ import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; import { putFlagReviewAction } from '@/services'; -import { useCaptchaModal, usePageUsers } from '@/hooks'; +import { usePageUsers } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { Editor, TagSelector, Mentions, TextArea } from '@/components'; import { // matchedUsers, @@ -89,7 +90,7 @@ const Index: FC = ({ const [loaded, setLoaded] = useState(false); const pageUsers = usePageUsers(); - const editCaptcha = useCaptchaModal('edit'); + const editCaptcha = useCaptchaPlugin('edit'); const onClose = (bol) => { if (bol) { @@ -160,14 +161,7 @@ const Index: FC = ({ return bol; }; - const handleSubmit = (event: React.FormEvent) => { - event.preventDefault(); - event.stopPropagation(); - - if (!checkValidated()) { - return; - } - + const submitFlagReviewAction = () => { const params: Type.PutFlagReviewParams = { title: formData.title.value, content: formData.content.value, @@ -191,28 +185,41 @@ const Index: FC = ({ delete params.title; delete params.tags; } - - editCaptcha.check(() => { - if (objectType === 'question') { - const imgCode = editCaptcha.getCaptcha(); - if (imgCode.verify) { - params.captcha_code = imgCode.captcha_code; - params.captcha_id = imgCode.captcha_id; - } + if (objectType === 'question') { + const imgCode = editCaptcha?.getCaptcha(); + if (imgCode?.verify) { + params.captcha_code = imgCode.captcha_code; + params.captcha_id = imgCode.captcha_id; } - putFlagReviewAction(params) - .then(async () => { - await editCaptcha.close(); - onClose(true); - }) - .catch((err) => { - if (err.isError) { - editCaptcha.handleCaptchaError(err.list); - const data = handleFormError(err, formData); - setFormData({ ...data }); - } - }); - }); + } + putFlagReviewAction(params) + .then(async () => { + await editCaptcha?.close(); + onClose(true); + }) + .catch((err) => { + if (err.isError) { + editCaptcha?.handleCaptchaError(err.list); + const data = handleFormError(err, formData); + setFormData({ ...data }); + } + }); + }; + + const handleSubmit = (event: React.FormEvent) => { + event.preventDefault(); + event.stopPropagation(); + + if (!checkValidated()) { + return; + } + + if (!editCaptcha) { + submitFlagReviewAction(); + return; + } + + editCaptcha.check(() => submitFlagReviewAction()); }; const handleSelected = (val) => { diff --git a/ui/src/pages/Search/index.tsx b/ui/src/pages/Search/index.tsx index 8517172de..a016af707 100644 --- a/ui/src/pages/Search/index.tsx +++ b/ui/src/pages/Search/index.tsx @@ -22,7 +22,8 @@ import { useTranslation } from 'react-i18next'; import { useSearchParams } from 'react-router-dom'; import { useEffect, useState } from 'react'; -import { usePageTags, useCaptchaModal, useSkeletonControl } from '@/hooks'; +import { usePageTags, useSkeletonControl } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { Pagination } from '@/components'; import { getSearchResult } from '@/services'; import type { SearchParams, SearchRes } from '@/common/interface'; @@ -51,7 +52,7 @@ const Index = () => { }); const { count = 0, list = [], extra = null } = data || {}; - const searchCaptcha = useCaptchaModal('search'); + const searchCaptcha = useCaptchaPlugin('search'); const doSearch = () => { setIsLoading(true); @@ -62,7 +63,7 @@ const Index = () => { size: 20, }; - const captcha = searchCaptcha.getCaptcha(); + const captcha = searchCaptcha?.getCaptcha(); if (captcha?.verify) { params.captcha_id = captcha.captcha_id; params.captcha_code = captcha.captcha_code; @@ -70,12 +71,12 @@ const Index = () => { getSearchResult(params) .then(async (resp) => { - await searchCaptcha.close(); + await searchCaptcha?.close(); setData(resp); }) .catch((err) => { if (err.isError) { - searchCaptcha.handleCaptchaError(err.list); + searchCaptcha?.handleCaptchaError(err.list); } }) .finally(() => { @@ -84,6 +85,10 @@ const Index = () => { }; useEffect(() => { + if (!searchCaptcha) { + doSearch(); + return; + } searchCaptcha.check(() => { doSearch(); }); diff --git a/ui/src/pages/SideNavLayout/index.tsx b/ui/src/pages/SideNavLayout/index.tsx index 268401580..773d572c3 100644 --- a/ui/src/pages/SideNavLayout/index.tsx +++ b/ui/src/pages/SideNavLayout/index.tsx @@ -27,8 +27,8 @@ import '@/common/sideNavLayout.scss'; const Index: FC = () => { return ( - - + + diff --git a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx index acca34aa9..5d7d2c9bb 100644 --- a/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx +++ b/ui/src/pages/Users/AccountForgot/components/sendEmail.tsx @@ -24,7 +24,7 @@ import { useTranslation } from 'react-i18next'; import type { PasswordResetReq, FormDataType } from '@/common/interface'; import { resetPassword } from '@/services'; import { handleFormError } from '@/utils'; -import { useCaptchaModal } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; interface IProps { // eslint-disable-next-line react/no-unused-prop-types @@ -42,7 +42,7 @@ const Index: FC = ({ callback }) => { }, }); - const emailCaptcha = useCaptchaModal('email'); + const emailCaptcha = useCaptchaPlugin('email'); const handleChange = (params: FormDataType) => { setFormData({ ...formData, ...params }); @@ -73,20 +73,20 @@ const Index: FC = ({ callback }) => { e_mail: formData.e_mail.value, }; - const captcha = emailCaptcha.getCaptcha(); - if (captcha.verify) { + const captcha = emailCaptcha?.getCaptcha(); + if (captcha?.verify) { params.captcha_code = captcha.captcha_code; params.captcha_id = captcha.captcha_id; } resetPassword(params) .then(async () => { - await emailCaptcha.close(); + await emailCaptcha?.close(); callback?.(2, formData.e_mail.value); }) .catch((err) => { if (err.isError) { - emailCaptcha.handleCaptchaError(err.list); + emailCaptcha?.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); } @@ -101,6 +101,11 @@ const Index: FC = ({ callback }) => { return; } + if (!emailCaptcha) { + sendEmail(); + return; + } + emailCaptcha.check(() => { sendEmail(); }); diff --git a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx index cf321a1ff..419e226e1 100644 --- a/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx +++ b/ui/src/pages/Users/ChangeEmail/components/sendEmail.tsx @@ -26,7 +26,7 @@ import type { PasswordResetReq, FormDataType } from '@/common/interface'; import { loggedUserInfoStore } from '@/stores'; import { changeEmail } from '@/services'; import { handleFormError } from '@/utils'; -import { useCaptchaModal } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; const Index: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'change_email' }); @@ -41,7 +41,7 @@ const Index: FC = () => { const navigate = useNavigate(); const { user: userInfo, update: updateUser } = loggedUserInfoStore(); - const emailCaptcha = useCaptchaModal('email'); + const emailCaptcha = useCaptchaPlugin('email'); const handleChange = (params: FormDataType) => { setFormData({ ...formData, ...params }); @@ -71,22 +71,22 @@ const Index: FC = () => { const params: PasswordResetReq = { e_mail: formData.e_mail.value, }; - const imgCode = emailCaptcha.getCaptcha(); - if (imgCode.verify) { + const imgCode = emailCaptcha?.getCaptcha(); + if (imgCode?.verify) { params.captcha_code = imgCode.captcha_code; params.captcha_id = imgCode.captcha_id; } changeEmail(params) .then(async () => { - await emailCaptcha.close(); + await emailCaptcha?.close(); userInfo.e_mail = formData.e_mail.value; updateUser(userInfo); navigate('/users/login', { replace: true }); }) .catch((err) => { if (err.isError) { - emailCaptcha.handleCaptchaError(err.list); + emailCaptcha?.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); } @@ -99,7 +99,10 @@ const Index: FC = () => { if (!checkValidated()) { return; } - + if (!emailCaptcha) { + sendEmail(); + return; + } emailCaptcha.check(() => { sendEmail(); }); diff --git a/ui/src/pages/Users/Login/index.tsx b/ui/src/pages/Users/Login/index.tsx index bbb1f67fa..52bbd00e4 100644 --- a/ui/src/pages/Users/Login/index.tsx +++ b/ui/src/pages/Users/Login/index.tsx @@ -22,7 +22,7 @@ import { Container, Form, Button, Col } from 'react-bootstrap'; import { Link, useNavigate, useSearchParams } from 'react-router-dom'; import { Trans, useTranslation } from 'react-i18next'; -import { usePageTags, useCaptchaModal } from '@/hooks'; +import { usePageTags } from '@/hooks'; import type { LoginReqParams, FormDataType } from '@/common/interface'; import { Unactivate, WelcomeTitle, PluginRender } from '@/components'; import { @@ -31,6 +31,7 @@ import { userCenterStore, } from '@/stores'; import { floppyNavigation, guard, handleFormError, userCenter } from '@/utils'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { login, UcAgent } from '@/services'; import { setupAppTheme } from '@/utils/localize'; @@ -68,7 +69,7 @@ const Index: React.FC = () => { setFormData({ ...formData, ...params }); }; - const passwordCaptcha = useCaptchaModal('password'); + const passwordCaptcha = useCaptchaPlugin('password'); const checkValidated = (): boolean => { let bol = true; @@ -107,7 +108,7 @@ const Index: React.FC = () => { pass: formData.pass.value, }; - const captcha = passwordCaptcha.getCaptcha(); + const captcha = passwordCaptcha?.getCaptcha(); if (captcha?.verify) { params.captcha_code = captcha.captcha_code; params.captcha_id = captcha.captcha_id; @@ -115,7 +116,7 @@ const Index: React.FC = () => { login(params) .then(async (res) => { - await passwordCaptcha.close(); + await passwordCaptcha?.close?.(); updateUser(res); setupAppTheme(); const userStat = guard.deriveLoginState(); @@ -130,7 +131,7 @@ const Index: React.FC = () => { if (err.isError) { const data = handleFormError(err, formData); setFormData({ ...data }); - passwordCaptcha.handleCaptchaError(err.list); + passwordCaptcha?.handleCaptchaError?.(err.list); } }); }; @@ -143,7 +144,12 @@ const Index: React.FC = () => { return; } - passwordCaptcha.check(() => { + if (!passwordCaptcha) { + handleLogin(); + return; + } + + passwordCaptcha?.check?.(() => { handleLogin(); }); }; @@ -165,6 +171,17 @@ const Index: React.FC = () => { {step === 1 ? ( + + + {ucAgentInfo ? ( = ({ callback }) => { }); const updateUser = userStore((state) => state.update); - const emailCaptcha = useCaptchaModal('email'); + const emailCaptcha = useCaptchaPlugin('email'); const handleChange = (params: FormDataType) => { setFormData({ ...formData, ...params }); @@ -99,7 +99,7 @@ const Index: React.FC = ({ callback }) => { pass: formData.pass.value, }; - const captcha = emailCaptcha.getCaptcha(); + const captcha = emailCaptcha?.getCaptcha(); if (captcha?.verify) { reqParams.captcha_code = captcha.captcha_code; reqParams.captcha_id = captcha.captcha_id; @@ -107,13 +107,13 @@ const Index: React.FC = ({ callback }) => { register(reqParams) .then(async (res) => { - await emailCaptcha.close(); + await emailCaptcha?.close(); updateUser(res); callback(); }) .catch((err) => { if (err.isError) { - emailCaptcha.handleCaptchaError(err.list); + emailCaptcha?.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); } @@ -126,6 +126,10 @@ const Index: React.FC = ({ callback }) => { if (!checkValidated()) { return; } + if (!emailCaptcha) { + handleRegister(); + return; + } emailCaptcha.check(() => { handleRegister(); }); diff --git a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx index 5672782b4..27eda1812 100644 --- a/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx +++ b/ui/src/pages/Users/Settings/Account/components/ModifyEmail/index.tsx @@ -22,7 +22,8 @@ import { Form, Button } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import type * as Type from '@/common/interface'; -import { useToast, useCaptchaModal } from '@/hooks'; +import { useToast } from '@/hooks'; +import { useCaptchaPlugin } from '@/utils/pluginKit'; import { getLoggedUserInfo, changeEmail } from '@/services'; import { handleFormError } from '@/utils'; @@ -45,7 +46,7 @@ const Index: FC = () => { }); const [userInfo, setUserInfo] = useState(); const toast = useToast(); - const emailCaptcha = useCaptchaModal('edit_userinfo'); + const emailCaptcha = useCaptchaPlugin('edit_userinfo'); useEffect(() => { getLoggedUserInfo().then((resp) => { @@ -66,7 +67,7 @@ const Index: FC = () => { formData.e_mail = { value: '', isInvalid: true, - errorMsg: t('email.msg'), + errorMsg: t('new_email.msg'), }; } @@ -108,14 +109,14 @@ const Index: FC = () => { pass: formData.pass.value, }; - const imgCode = emailCaptcha.getCaptcha(); - if (imgCode.verify) { + const imgCode = emailCaptcha?.getCaptcha(); + if (imgCode?.verify) { params.captcha_code = imgCode.captcha_code; params.captcha_id = imgCode.captcha_id; } changeEmail(params) .then(async () => { - await emailCaptcha.close(); + await emailCaptcha?.close(); setStep(1); toast.onShow({ msg: t('change_email_info'), @@ -125,7 +126,7 @@ const Index: FC = () => { }) .catch((err) => { if (err.isError) { - emailCaptcha.handleCaptchaError(err.list); + emailCaptcha?.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); } @@ -138,7 +139,10 @@ const Index: FC = () => { if (!checkValidated()) { return; } - + if (!emailCaptcha) { + postEmail(); + return; + } emailCaptcha.check(() => { postEmail(); }); @@ -194,7 +198,7 @@ const Index: FC = () => { - {t('email.label')} + {t('new_email.label')} { }, }); - const infoCaptcha = useCaptchaModal('edit_userinfo'); + const infoCaptcha = useCaptchaPlugin('edit_userinfo'); const handleFormState = () => { setFormState((pre) => !pre); @@ -134,14 +135,14 @@ const Index: FC = () => { pass: formData.pass.value, }; - const imgCode = infoCaptcha.getCaptcha(); - if (imgCode.verify) { + const imgCode = infoCaptcha?.getCaptcha(); + if (imgCode?.verify) { params.captcha_code = imgCode.captcha_code; params.captcha_id = imgCode.captcha_id; } modifyPassword(params) .then(async () => { - await infoCaptcha.close(); + await infoCaptcha?.close(); toast.onShow({ msg: t('update_password', { keyPrefix: 'toast' }), variant: 'success', @@ -150,7 +151,7 @@ const Index: FC = () => { }) .catch((err) => { if (err.isError) { - infoCaptcha.handleCaptchaError(err.list); + infoCaptcha?.handleCaptchaError(err.list); const data = handleFormError(err, formData); setFormData({ ...data }); } @@ -163,6 +164,10 @@ const Index: FC = () => { if (!checkValidated()) { return; } + if (!infoCaptcha) { + postModifyPass(); + return; + } infoCaptcha.check(() => { postModifyPass(); diff --git a/ui/src/plugins/builtin/HostingConnector/index.tsx b/ui/src/plugins/builtin/HostingConnector/index.tsx index bcddf3625..ae0986372 100644 --- a/ui/src/plugins/builtin/HostingConnector/index.tsx +++ b/ui/src/plugins/builtin/HostingConnector/index.tsx @@ -23,8 +23,11 @@ import { useTranslation } from 'react-i18next'; import classnames from 'classnames'; -import { PluginInfo } from '@/utils/pluginKit'; -import { getTransNs, getTransKeyPrefix } from '@/utils/pluginKit/utils'; +import { + getTransNs, + getTransKeyPrefix, + PluginInfo, +} from '@/utils/pluginKit/utils'; import { SvgIcon } from '@/components'; import { userCenterStore } from '@/stores'; import './i18n'; diff --git a/ui/src/plugins/builtin/SearchInfo/index.tsx b/ui/src/plugins/builtin/SearchInfo/index.tsx index e57819755..24db01676 100644 --- a/ui/src/plugins/builtin/SearchInfo/index.tsx +++ b/ui/src/plugins/builtin/SearchInfo/index.tsx @@ -20,8 +20,11 @@ import { memo, FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { PluginInfo } from '@/utils/pluginKit'; -import { getTransNs, getTransKeyPrefix } from '@/utils/pluginKit/utils'; +import { + getTransNs, + getTransKeyPrefix, + PluginInfo, +} from '@/utils/pluginKit/utils'; import { SvgIcon } from '@/components'; import info from './info.yaml'; diff --git a/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx b/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx index 99032b208..14ba7c0c1 100644 --- a/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx +++ b/ui/src/plugins/builtin/ThirdPartyConnector/index.tsx @@ -23,8 +23,11 @@ import { useTranslation } from 'react-i18next'; import classnames from 'classnames'; -import { PluginInfo } from '@/utils/pluginKit'; -import { getTransNs, getTransKeyPrefix } from '@/utils/pluginKit/utils'; +import { + getTransNs, + getTransKeyPrefix, + PluginInfo, +} from '@/utils/pluginKit/utils'; import { SvgIcon } from '@/components'; import info from './info.yaml'; diff --git a/ui/src/router/index.tsx b/ui/src/router/index.tsx index dca1e1bee..4cb222a97 100644 --- a/ui/src/router/index.tsx +++ b/ui/src/router/index.tsx @@ -21,6 +21,7 @@ import { Suspense, lazy } from 'react'; import { RouteObject } from 'react-router-dom'; import Layout from '@/pages/Layout'; +import { mergeRoutePlugins } from '@/utils/pluginKit'; import baseRoutes, { RouteNode } from './routes'; import RouteGuard from './RouteGuard'; @@ -69,5 +70,6 @@ const routeWrapper = (routeNodes: RouteNode[], root: RouteNode[]) => { }; routeWrapper(baseRoutes, routes); +const mergedRoutes = mergeRoutePlugins(routes); -export default routes as RouteObject[]; +export default mergedRoutes as RouteObject[]; diff --git a/ui/src/router/pathFactory.ts b/ui/src/router/pathFactory.ts index ea3536041..256dcbf03 100644 --- a/ui/src/router/pathFactory.ts +++ b/ui/src/router/pathFactory.ts @@ -34,7 +34,7 @@ const tagEdit = (tagId: string) => { return r; }; -const questionLanding = (questionId: string, slugTitle: string = '') => { +const questionLanding = (questionId: string = '', slugTitle: string = '') => { const { seo } = seoSettingStore.getState(); if (!questionId) { return slugTitle ? `/questions/null/${slugTitle}` : '/questions/null'; diff --git a/ui/src/utils/pluginKit/index.ts b/ui/src/utils/pluginKit/index.ts index dfc1f736e..eca058bdc 100644 --- a/ui/src/utils/pluginKit/index.ts +++ b/ui/src/utils/pluginKit/index.ts @@ -17,14 +17,16 @@ * under the License. */ -import { NamedExoticComponent, FC } from 'react'; +import React from 'react'; import builtin from '@/plugins/builtin'; import * as allPlugins from '@/plugins'; import type * as Type from '@/common/interface'; +import { LOGGED_TOKEN_STORAGE_KEY } from '@/common/constants'; import { getPluginsStatus } from '@/services'; +import Storage from '@/utils/storage'; -import { initI18nResource } from './utils'; +import { initI18nResource, Plugin, PluginInfo, PluginType } from './utils'; /** * This information is to be defined for all components. @@ -38,24 +40,6 @@ import { initI18nResource } from './utils'; * @field description: Plugin description, optionally configurable. Usually read from the `i18n` file */ -export type PluginType = 'connector' | 'search' | 'editor'; -export interface PluginInfo { - slug_name: string; - type: PluginType; - name?: string; - description?: string; -} - -export interface Plugin { - info: PluginInfo; - component: NamedExoticComponent | FC; - i18nConfig?; - hooks?: { - useRender?: Array<(element: HTMLElement | null) => void>; - }; - activated?: boolean; -} - class Plugins { plugins: Plugin[] = []; @@ -137,6 +121,11 @@ class Plugins { return this.plugins.find((p) => p.info.slug_name === slug_name); } + getOnePluginHooks(slug_name: string) { + const plugin = this.getPlugin(slug_name); + return plugin?.hooks; + } + getPlugins() { return this.plugins; } @@ -155,5 +144,56 @@ const useRenderHtmlPlugin = (element: HTMLElement | null) => { }); }; -export { useRenderHtmlPlugin }; +const getRoutePlugins = () => { + return plugins.getPlugins().filter((plugin) => plugin.info.type === 'route'); +}; + +const defaultProps = () => { + const token = Storage.get(LOGGED_TOKEN_STORAGE_KEY) || ''; + return { + key: token, + headers: { + Authorization: token, + }, + }; +}; + +const mergeRoutePlugins = (routes) => { + const routePlugins = getRoutePlugins(); + if (routePlugins.length === 0) { + return routes; + } + routes.forEach((route) => { + if (route.page === 'pages/Layout') { + route.children?.forEach((child) => { + if (child.page === 'pages/SideNavLayout') { + routePlugins.forEach((plugin) => { + const { slug_name, route: path } = plugin.info; + const Component = plugin.component; + + child.children.push({ + page: `plugin/${slug_name}`, + path, + element: React.createElement(Component, defaultProps(), null), + }); + }); + } + }); + } + }); + return routes; +}; + +// Only one captcha type plug-in can be enabled at the same time +const useCaptchaPlugin = (key: Type.CaptchaKey) => { + const captcha = plugins + .getPlugins() + .filter((plugin) => plugin.info.type === 'captcha'); + const pluginHooks = plugins.getOnePluginHooks(captcha[0]?.info.slug_name); + return pluginHooks?.useCaptcha?.(key); +}; + +export type { Plugin, PluginInfo, PluginType }; + +export { useRenderHtmlPlugin, mergeRoutePlugins, useCaptchaPlugin }; export default plugins; diff --git a/ui/src/utils/pluginKit/interface.ts b/ui/src/utils/pluginKit/interface.ts new file mode 100644 index 000000000..d43767649 --- /dev/null +++ b/ui/src/utils/pluginKit/interface.ts @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export type PluginType = 'connector' | 'search' | 'editor' | 'captcha'; + +export interface PluginInfo { + slug_name: string; + type: PluginType; + name?: string; + description?: string; +} diff --git a/ui/src/utils/pluginKit/utils.ts b/ui/src/utils/pluginKit/utils.ts index 76bbf3880..a6ca44cea 100644 --- a/ui/src/utils/pluginKit/utils.ts +++ b/ui/src/utils/pluginKit/utils.ts @@ -21,6 +21,8 @@ import { NamedExoticComponent, FC } from 'react'; import i18next from 'i18next'; +import type * as Type from '@/common/interface'; + /** * This information is to be defined for all components. * It may be used for feature upgrades or version compatibility processing. @@ -35,17 +37,35 @@ import i18next from 'i18next'; const I18N_NS = 'plugin'; -export type PluginType = 'connector' | 'search' | 'editor'; +export type PluginType = + | 'connector' + | 'search' + | 'editor' + | 'route' + | 'captcha'; export interface PluginInfo { slug_name: string; type: PluginType; name?: string; description?: string; + route?: string; } export interface Plugin { info: PluginInfo; component: NamedExoticComponent | FC; + i18nConfig?; + hooks?: { + useRender?: Array<(element: HTMLElement | null) => void>; + useCaptcha?: (key: Type.CaptchaKey) => { + getCaptcha: () => Record; + check: (t: () => void) => void; + handleCaptchaError: (error) => any; + close: () => Promise; + resolveCaptchaReq: (data) => void; + }; + }; + activated?: boolean; } interface I18nResource {