From 0339b36330a6ded608e6a11ee1a5eb7e637b4add Mon Sep 17 00:00:00 2001 From: Hosein Beigi Date: Tue, 27 Feb 2024 11:00:35 +0330 Subject: [PATCH 01/15] feat/add-persian --- i18n/i18n.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/i18n/i18n.yaml b/i18n/i18n.yaml index fa8d5aec..190ac0bf 100644 --- a/i18n/i18n.yaml +++ b/i18n/i18n.yaml @@ -59,3 +59,6 @@ language_options: - label: "Slovak" value: "sk_SK" progress: 62 + - label: "فارسی" + value: "fa_IR" + progress: 85 From bee1f5624e7789fdb9bf113cff9e54acfbea9c66 Mon Sep 17 00:00:00 2001 From: hgaol Date: Fri, 1 Mar 2024 22:01:06 +0800 Subject: [PATCH 02/15] fix: remove delete action for deleted question even for admin --- internal/service/permission/question_permission.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/service/permission/question_permission.go b/internal/service/permission/question_permission.go index 1df244e3..b93cdfc1 100644 --- a/internal/service/permission/question_permission.go +++ b/internal/service/permission/question_permission.go @@ -92,7 +92,8 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str Type: "confirm", }) } - if canDelete || userID == creatorUserID { + + if (canDelete || userID == creatorUserID) && status != entity.QuestionStatusDeleted { actions = append(actions, &schema.PermissionMemberAction{ Action: "delete", Name: translator.Tr(lang, deleteActionName), From d26079a1886826803bc19075b615e7eb2114507e Mon Sep 17 00:00:00 2001 From: hgaol Date: Mon, 4 Mar 2024 22:10:47 +0800 Subject: [PATCH 03/15] fix: can't search username with '-' charactor --- internal/schema/search_schema.go | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/schema/search_schema.go b/internal/schema/search_schema.go index 90b08ceb..786fb9bb 100644 --- a/internal/schema/search_schema.go +++ b/internal/schema/search_schema.go @@ -20,11 +20,12 @@ package schema import ( + "regexp" + "strings" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/base/validator" "github.com/apache/incubator-answer/plugin" - "regexp" - "strings" ) type SearchDTO struct { @@ -40,7 +41,7 @@ type SearchDTO struct { func (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) { // Replace special characters. // Special characters will cause the search abnormal, such as search for "#" will get nearly all the content that Markdown format. - s.Query = regexp.MustCompile(`[+#.<>\-_()*]`).ReplaceAllString(s.Query, " ") + s.Query = regexp.MustCompile(`[+#.<>_()*]`).ReplaceAllString(s.Query, " ") s.Query = regexp.MustCompile(`\s+`).ReplaceAllString(s.Query, " ") s.Query = strings.TrimSpace(s.Query) return nil, nil From 1934ddbe27240c7768642c30ba4f0d3bb7bf4d3d Mon Sep 17 00:00:00 2001 From: hgaol Date: Tue, 5 Mar 2024 00:34:23 +0800 Subject: [PATCH 04/15] update --- internal/schema/search_schema.go | 28 ++++++++++++++++++++++++--- internal/schema/search_schema_test.go | 22 +++++++++++++++++++++ 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 internal/schema/search_schema_test.go diff --git a/internal/schema/search_schema.go b/internal/schema/search_schema.go index 786fb9bb..59f32c5e 100644 --- a/internal/schema/search_schema.go +++ b/internal/schema/search_schema.go @@ -41,12 +41,34 @@ type SearchDTO struct { func (s *SearchDTO) Check() (errField []*validator.FormErrorField, err error) { // Replace special characters. // Special characters will cause the search abnormal, such as search for "#" will get nearly all the content that Markdown format. - s.Query = regexp.MustCompile(`[+#.<>_()*]`).ReplaceAllString(s.Query, " ") - s.Query = regexp.MustCompile(`\s+`).ReplaceAllString(s.Query, " ") - s.Query = strings.TrimSpace(s.Query) + replacedContent, patterns := ReplaceSearchContent(s.Query) + s.Query = strings.Join(append(patterns, replacedContent), " ") + return nil, nil } +func ReplaceSearchContent(content string) (string, []string) { + // Define the regular expressions for key:value pairs and [tag] + keyValueRegex := regexp.MustCompile(`\w+:\S+`) + tagRegex := regexp.MustCompile(`\[\w+\]`) + // Define the pattern for characters to replace + replaceCharsPattern := regexp.MustCompile(`[+#.<>\-_()*]`) + + // Extract key:value pairs + keyValues := keyValueRegex.FindAllString(content, -1) + // Extract [tag] + tags := tagRegex.FindAllString(content, -1) + + // Replace key:value pairs and [tag] with empty string + contentWithoutPatterns := keyValueRegex.ReplaceAllString(content, "") + contentWithoutPatterns = tagRegex.ReplaceAllString(contentWithoutPatterns, "") + + // Replace characters with pattern [+#.<>_()*] with space + replacedContent := replaceCharsPattern.ReplaceAllString(contentWithoutPatterns, " ") + + return strings.TrimSpace(replacedContent), append(keyValues, tags...) +} + type SearchCondition struct { // search target type: all/question/answer TargetType string diff --git a/internal/schema/search_schema_test.go b/internal/schema/search_schema_test.go new file mode 100644 index 00000000..1549f0e1 --- /dev/null +++ b/internal/schema/search_schema_test.go @@ -0,0 +1,22 @@ +package schema + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestReplaceSearchContent(t *testing.T) { + content := "user:aaa [tag] ssssfdfdf-as#fsadf" + replacedContent, patterns := ReplaceSearchContent(content) + ret := strings.Join(append(patterns, replacedContent), " ") + + assert.Equal(t, "user:aaa [tag]ssssfdfdf as fsadf", ret) + + content = "user:aaa-sss [tag1] ssssfdfdf-as#fsadf [tag2] score:3" + replacedContent, patterns = ReplaceSearchContent(content) + ret = strings.Join(append(patterns, replacedContent), " ") + + assert.Equal(t, "user:aaa-sss score:3 [tag1] [tag2] ssssfdfdf as fsadf", ret) +} From 16cafbaf95bbc7d2a919fdc92c4771b7833186d7 Mon Sep 17 00:00:00 2001 From: hgaol Date: Tue, 5 Mar 2024 00:36:53 +0800 Subject: [PATCH 05/15] update --- internal/schema/search_schema_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/schema/search_schema_test.go b/internal/schema/search_schema_test.go index 1549f0e1..50ffdf14 100644 --- a/internal/schema/search_schema_test.go +++ b/internal/schema/search_schema_test.go @@ -12,7 +12,7 @@ func TestReplaceSearchContent(t *testing.T) { replacedContent, patterns := ReplaceSearchContent(content) ret := strings.Join(append(patterns, replacedContent), " ") - assert.Equal(t, "user:aaa [tag]ssssfdfdf as fsadf", ret) + assert.Equal(t, "user:aaa [tag] ssssfdfdf as fsadf", ret) content = "user:aaa-sss [tag1] ssssfdfdf-as#fsadf [tag2] score:3" replacedContent, patterns = ReplaceSearchContent(content) From 3e1b20e9fb8ae9dfa1b12af3c6a0d63ff53b77ff Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Mon, 4 Mar 2024 17:30:29 +0800 Subject: [PATCH 06/15] feat: Add a order filter called "Oldest" in answer --- i18n/en_US.yaml | 1 + i18n/zh_CN.yaml | 1 + internal/entity/answer_entity.go | 1 + internal/repo/answer/answer_repo.go | 2 ++ ui/src/common/interface.ts | 2 +- .../Questions/Detail/components/AnswerHead/index.tsx | 12 +++++++++++- ui/src/pages/Questions/Detail/index.tsx | 2 +- 7 files changed, 18 insertions(+), 3 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 2d595775..484d62d1 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1079,6 +1079,7 @@ ui: title: Answers score: Score newest: Newest + oldest: Oldest btn_accept: Accept btn_accepted: Accepted write_answer: diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index a0b84535..f204860a 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1050,6 +1050,7 @@ ui: title: 个回答 score: 评分 newest: 最新 + oldest: 最旧 btn_accept: 采纳 btn_accepted: 已被采纳 write_answer: diff --git a/internal/entity/answer_entity.go b/internal/entity/answer_entity.go index e3520b8c..5eec9728 100644 --- a/internal/entity/answer_entity.go +++ b/internal/entity/answer_entity.go @@ -25,6 +25,7 @@ const ( AnswerSearchOrderByDefault = "default" AnswerSearchOrderByTime = "updated" AnswerSearchOrderByVote = "vote" + AnswerSearchOrderByTimeAsc = "created" AnswerStatusAvailable = 1 AnswerStatusDeleted = 10 diff --git a/internal/repo/answer/answer_repo.go b/internal/repo/answer/answer_repo.go index aca7517c..b34a9600 100644 --- a/internal/repo/answer/answer_repo.go +++ b/internal/repo/answer/answer_repo.go @@ -336,6 +336,8 @@ func (ar *answerRepo) SearchList(ctx context.Context, search *entity.AnswerSearc switch search.Order { case entity.AnswerSearchOrderByTime: session = session.OrderBy("created_at desc") + case entity.AnswerSearchOrderByTimeAsc: + session = session.OrderBy("created_at asc") case entity.AnswerSearchOrderByVote: session = session.OrderBy("vote_count desc") default: diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 201a5ae0..71b14080 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -250,7 +250,7 @@ export interface QuestionDetailRes { } export interface AnswersReq extends Paging { - order?: 'default' | 'updated'; + order?: 'default' | 'updated' | 'created'; question_id: string; } diff --git a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx index c15f44e1..bc81ec37 100644 --- a/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx +++ b/ui/src/pages/Questions/Detail/components/AnswerHead/index.tsx @@ -36,6 +36,10 @@ const sortBtns = [ name: 'newest', sort: 'updated', }, + { + name: 'oldest', + sort: 'created', + }, ]; const Index: FC = ({ count = 0, order = 'default' }) => { @@ -52,7 +56,13 @@ const Index: FC = ({ count = 0, order = 'default' }) => { diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index aac8e184..e0908eac 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -92,7 +92,7 @@ const Index = () => { const requestAnswers = async () => { const res = await getAnswers({ - order: order === 'updated' ? order : 'default', + order: order === 'updated' || order === 'created' ? order : 'default', question_id: qid, page: 1, page_size: 999, From c014be823b494007c379ece3416512fa9067cef1 Mon Sep 17 00:00:00 2001 From: sy-records <52o@qq52o.cn> Date: Tue, 5 Mar 2024 17:14:18 +0800 Subject: [PATCH 07/15] chore: sync swagger --- docs/docs.go | 46 ++++++++++++++++++++++++++++++++++++++++++---- docs/swagger.json | 46 ++++++++++++++++++++++++++++++++++++++++++---- docs/swagger.yaml | 30 +++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 9 deletions(-) diff --git a/docs/docs.go b/docs/docs.go index dc56880a..ee259828 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -6554,7 +6554,8 @@ const docTemplate = `{ "type": "string" }, "value": { - "type": "integer" + "type": "integer", + "minimum": 1 } } }, @@ -7094,6 +7095,12 @@ const docTemplate = `{ "action": { "$ref": "#/definitions/schema.UIOptionAction" }, + "class_name": { + "type": "string" + }, + "field_class_name": { + "type": "string" + }, "input_type": { "type": "string" }, @@ -7374,6 +7381,10 @@ const docTemplate = `{ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" @@ -8161,12 +8172,14 @@ const docTemplate = `{ "enum": [ 1, 2, - 3 + 3, + 99 ], "x-enum-varnames": [ "PrivilegeLevel1", "PrivilegeLevel2", - "PrivilegeLevel3" + "PrivilegeLevel3", + "PrivilegeLevelCustom" ] }, "schema.PrivilegeOption": { @@ -9074,6 +9087,10 @@ const docTemplate = `{ "theme" ], "properties": { + "color_scheme": { + "type": "string", + "maxLength": 100 + }, "theme": { "type": "string", "maxLength": 255 @@ -9087,6 +9104,9 @@ const docTemplate = `{ "schema.SiteThemeResp": { "type": "object", "properties": { + "color_scheme": { + "type": "string" + }, "theme": { "type": "string" }, @@ -9435,8 +9455,13 @@ const docTemplate = `{ "level" ], "properties": { + "custom_privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } + }, "level": { - "maximum": 3, "minimum": 1, "allOf": [ { @@ -9542,9 +9567,15 @@ const docTemplate = `{ "schema.UpdateUserInterfaceRequest": { "type": "object", "required": [ + "color_scheme", "language" ], "properties": { + "color_scheme": { + "description": "Color scheme", + "type": "string", + "maxLength": 100 + }, "language": { "description": "language", "type": "string", @@ -9652,6 +9683,9 @@ const docTemplate = `{ "id": { "type": "string" }, + "language": { + "type": "string" + }, "location": { "type": "string" }, @@ -9757,6 +9791,10 @@ const docTemplate = `{ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" diff --git a/docs/swagger.json b/docs/swagger.json index 83bde221..d65e30bd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -6523,7 +6523,8 @@ "type": "string" }, "value": { - "type": "integer" + "type": "integer", + "minimum": 1 } } }, @@ -7063,6 +7064,12 @@ "action": { "$ref": "#/definitions/schema.UIOptionAction" }, + "class_name": { + "type": "string" + }, + "field_class_name": { + "type": "string" + }, "input_type": { "type": "string" }, @@ -7343,6 +7350,10 @@ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" @@ -8130,12 +8141,14 @@ "enum": [ 1, 2, - 3 + 3, + 99 ], "x-enum-varnames": [ "PrivilegeLevel1", "PrivilegeLevel2", - "PrivilegeLevel3" + "PrivilegeLevel3", + "PrivilegeLevelCustom" ] }, "schema.PrivilegeOption": { @@ -9043,6 +9056,10 @@ "theme" ], "properties": { + "color_scheme": { + "type": "string", + "maxLength": 100 + }, "theme": { "type": "string", "maxLength": 255 @@ -9056,6 +9073,9 @@ "schema.SiteThemeResp": { "type": "object", "properties": { + "color_scheme": { + "type": "string" + }, "theme": { "type": "string" }, @@ -9404,8 +9424,13 @@ "level" ], "properties": { + "custom_privileges": { + "type": "array", + "items": { + "$ref": "#/definitions/constant.Privilege" + } + }, "level": { - "maximum": 3, "minimum": 1, "allOf": [ { @@ -9511,9 +9536,15 @@ "schema.UpdateUserInterfaceRequest": { "type": "object", "required": [ + "color_scheme", "language" ], "properties": { + "color_scheme": { + "description": "Color scheme", + "type": "string", + "maxLength": 100 + }, "language": { "description": "language", "type": "string", @@ -9621,6 +9652,9 @@ "id": { "type": "string" }, + "language": { + "type": "string" + }, "location": { "type": "string" }, @@ -9726,6 +9760,10 @@ "description": "bio html", "type": "string" }, + "color_scheme": { + "description": "Color scheme", + "type": "string" + }, "created_at": { "description": "create time", "type": "integer" diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ca78bc49..21d72673 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -29,6 +29,7 @@ definitions: label: type: string value: + minimum: 1 type: integer type: object handler.RespBody: @@ -403,6 +404,10 @@ definitions: properties: action: $ref: '#/definitions/schema.UIOptionAction' + class_name: + type: string + field_class_name: + type: string input_type: type: string label: @@ -603,6 +608,9 @@ definitions: bio_html: description: bio html type: string + color_scheme: + description: Color scheme + type: string created_at: description: create time type: integer @@ -1156,11 +1164,13 @@ definitions: - 1 - 2 - 3 + - 99 type: integer x-enum-varnames: - PrivilegeLevel1 - PrivilegeLevel2 - PrivilegeLevel3 + - PrivilegeLevelCustom schema.PrivilegeOption: properties: level: @@ -1783,6 +1793,9 @@ definitions: type: object schema.SiteThemeReq: properties: + color_scheme: + maxLength: 100 + type: string theme: maxLength: 255 type: string @@ -1794,6 +1807,8 @@ definitions: type: object schema.SiteThemeResp: properties: + color_scheme: + type: string theme: type: string theme_config: @@ -2030,10 +2045,13 @@ definitions: type: object schema.UpdatePrivilegesConfigReq: properties: + custom_privileges: + items: + $ref: '#/definitions/constant.Privilege' + type: array level: allOf: - $ref: '#/definitions/schema.PrivilegeLevel' - maximum: 3 minimum: 1 required: - level @@ -2107,11 +2125,16 @@ definitions: type: object schema.UpdateUserInterfaceRequest: properties: + color_scheme: + description: Color scheme + maxLength: 100 + type: string language: description: language maxLength: 100 type: string required: + - color_scheme - language type: object schema.UpdateUserNotificationConfigReq: @@ -2183,6 +2206,8 @@ definitions: type: string id: type: string + language: + type: string location: type: string rank: @@ -2257,6 +2282,9 @@ definitions: bio_html: description: bio html type: string + color_scheme: + description: Color scheme + type: string created_at: description: create time type: integer From c6042d96edabb5e26f5ef7f69ac537a5de6acc60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=8C=E7=AB=8B?= Date: Tue, 5 Mar 2024 19:18:03 +0800 Subject: [PATCH 08/15] feat: filter the same question in similar questions --- internal/service/question_service.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/internal/service/question_service.go b/internal/service/question_service.go index a4d5ffe6..8677123b 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -1235,7 +1235,17 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin search.Tag = tagNames[0] } search.LoginUserID = loginUserID - return qs.GetQuestionPage(ctx, search) + similarQuestions, _ , err := qs.GetQuestionPage(ctx, search) + if err !=nil { + return nil, 0, err + } + var result []*schema.QuestionPageResp + for _, v := range similarQuestions { + if v.ID != questionID { + result = append(result, v) + } + } + return result, int64(len(result)), nil } // GetQuestionPage query questions page From 51b96ec06f9664bc1c006fa1ce697a25058a533a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=80=8C=E7=AB=8B?= Date: Wed, 6 Mar 2024 14:56:44 +0800 Subject: [PATCH 09/15] feat: format code --- internal/service/question_service.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/service/question_service.go b/internal/service/question_service.go index 8677123b..e01054b4 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -1235,16 +1235,16 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin search.Tag = tagNames[0] } search.LoginUserID = loginUserID - similarQuestions, _ , err := qs.GetQuestionPage(ctx, search) - if err !=nil { + similarQuestions, _, err := qs.GetQuestionPage(ctx, search) + if err != nil { return nil, 0, err } var result []*schema.QuestionPageResp - for _, v := range similarQuestions { - if v.ID != questionID { - result = append(result, v) - } - } + for _, v := range similarQuestions { + if v.ID != questionID { + result = append(result, v) + } + } return result, int64(len(result)), nil } From 5d777896e3c8f4d69cf6942424d53b630c5db3b9 Mon Sep 17 00:00:00 2001 From: foxzero-007 Date: Wed, 6 Mar 2024 15:30:43 +0800 Subject: [PATCH 10/15] feat: use uid.DeshortID on v.ID --- internal/service/question_service.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/service/question_service.go b/internal/service/question_service.go index e01054b4..5e8477a4 100644 --- a/internal/service/question_service.go +++ b/internal/service/question_service.go @@ -1241,7 +1241,7 @@ func (qs *QuestionService) SimilarQuestion(ctx context.Context, questionID strin } var result []*schema.QuestionPageResp for _, v := range similarQuestions { - if v.ID != questionID { + if uid.DeShortID(v.ID) != questionID { result = append(result, v) } } From 2a828c93c103ffeb72a50bde1664506af294da1d Mon Sep 17 00:00:00 2001 From: hgaol Date: Fri, 8 Mar 2024 09:44:57 +0800 Subject: [PATCH 11/15] feat: support setting config for check latest answer version --- i18n/en_US.yaml | 3 +++ i18n/zh_CN.yaml | 3 +++ internal/schema/siteinfo_schema.go | 1 + internal/service/dashboard/dashboard_service.go | 12 ++++++++++-- .../service/siteinfo_common/siteinfo_service.go | 2 +- ui/src/common/interface.ts | 2 ++ .../Dashboard/components/HealthStatus/index.tsx | 4 +++- ui/src/pages/Admin/General/index.tsx | 14 ++++++++++++++ ui/src/stores/siteInfo.ts | 1 + 9 files changed, 38 insertions(+), 4 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 484d62d1..18312e5e 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1595,6 +1595,9 @@ ui: msg: Contact email cannot be empty. validate: Contact email is not valid. text: Email address of key contact responsible for this site. + check_update: + label: Software updates + text: Automatically check for updates interface: page_title: Interface language: diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index f204860a..01bc07b9 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1550,6 +1550,9 @@ ui: msg: 联系人邮箱不能为空。 validate: 联系人邮箱无效。 text: 本网站的主要联系邮箱地址。 + check_update: + label: 软件更新 + text: 自动检查软件更新 interface: page_title: 界面 language: diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 2953b944..ea19d046 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -40,6 +40,7 @@ type SiteGeneralReq struct { Description string `validate:"omitempty,sanitizer,gt=3,lte=2000" form:"description" json:"description"` SiteUrl string `validate:"required,sanitizer,gt=1,lte=512,url" form:"site_url" json:"site_url"` ContactEmail string `validate:"required,sanitizer,gt=1,lte=512,email" form:"contact_email" json:"contact_email"` + CheckUpdate bool `validate:"omitempty,sanitizer" form:"check_update" json:"check_update"` } func (r *SiteGeneralReq) FormatSiteUrl() { diff --git a/internal/service/dashboard/dashboard_service.go b/internal/service/dashboard/dashboard_service.go index c9b6efe1..0d36cd6a 100644 --- a/internal/service/dashboard/dashboard_service.go +++ b/internal/service/dashboard/dashboard_service.go @@ -23,11 +23,12 @@ import ( "context" "encoding/json" "fmt" - "github.com/apache/incubator-answer/pkg/converter" "io" "net/http" "net/url" "time" + + "github.com/apache/incubator-answer/pkg/converter" "xorm.io/xorm/schemas" "github.com/apache/incubator-answer/internal/base/constant" @@ -101,7 +102,14 @@ func (ds *dashboardService) Statistical(ctx context.Context) (*schema.DashboardI dashboardInfo.ReportCount = ds.reportCount(ctx) dashboardInfo.VoteCount = ds.voteCount(ctx) dashboardInfo.OccupyingStorageSpace = ds.calculateStorage() - dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx) + general, err := ds.siteInfoService.GetSiteGeneral(ctx) + if err != nil { + log.Errorf("get general site info failed: %s", err) + return dashboardInfo, nil + } + if general.CheckUpdate { + dashboardInfo.VersionInfo.RemoteVersion = ds.remoteVersion(ctx) + } dashboardInfo.DatabaseVersion = ds.getDatabaseInfo() dashboardInfo.DatabaseSize = ds.GetDatabaseSize() } diff --git a/internal/service/siteinfo_common/siteinfo_service.go b/internal/service/siteinfo_common/siteinfo_service.go index e37fb7b1..2036ec6e 100644 --- a/internal/service/siteinfo_common/siteinfo_service.go +++ b/internal/service/siteinfo_common/siteinfo_service.go @@ -66,7 +66,7 @@ func NewSiteInfoCommonService(siteInfoRepo SiteInfoRepo) SiteInfoCommonService { // GetSiteGeneral get site info general func (s *siteInfoCommonService) GetSiteGeneral(ctx context.Context) (resp *schema.SiteGeneralResp, err error) { - resp = &schema.SiteGeneralResp{} + resp = &schema.SiteGeneralResp{CheckUpdate: true} if err = s.GetSiteInfoByType(ctx, constant.SiteTypeGeneral, resp); err != nil { return nil, err } diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 71b14080..65117ae1 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -347,6 +347,8 @@ export interface AdminSettingsGeneral { description: string; site_url: string; contact_email: string; + check_update: boolean; + permalink?: number; } export interface HelmetBase { diff --git a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx index c77f0bb1..64122669 100644 --- a/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx +++ b/ui/src/pages/Admin/Dashboard/components/HealthStatus/index.tsx @@ -23,6 +23,7 @@ import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import type * as Type from '@/common/interface'; +import { siteInfoStore } from '@/stores'; const { gt, gte } = require('semver'); @@ -33,6 +34,7 @@ interface IProps { const HealthStatus: FC = ({ data }) => { const { t } = useTranslation('translation', { keyPrefix: 'admin.dashboard' }); const { version, remote_version } = data.version_info || {}; + const { siteInfo } = siteInfoStore(); let isLatest = false; let hasNewerVersion = false; if (version && remote_version) { @@ -65,7 +67,7 @@ const HealthStatus: FC = ({ data }) => { {t('update_to')} {remote_version} )} - {!isLatest && !remote_version && ( + {!isLatest && !remote_version && siteInfo.check_update && ( { title: t('contact_email.label'), description: t('contact_email.text'), }, + check_update: { + type: 'boolean', + title: t('check_update.label'), + }, }, }; const uiSchema: UISchema = { @@ -107,6 +111,12 @@ const General: FC = () => { }, }, }, + check_update: { + 'ui:widget': 'switch', + 'ui:options': { + label: t('check_update.text'), + }, + }, }; const [formData, setFormData] = useState( initFormData(schema), @@ -121,6 +131,7 @@ const General: FC = () => { short_description: formData.short_description.value, site_url: formData.site_url.value, contact_email: formData.contact_email.value, + check_update: formData.check_update.value, }; updateGeneralSetting(reqParams) @@ -135,6 +146,7 @@ const General: FC = () => { formData.short_description.value = res.short_description; formData.site_url.value = res.site_url; formData.contact_email.value = res.contact_email; + formData.check_update.value = res.check_update; } setFormData({ ...formData }); @@ -156,10 +168,12 @@ const General: FC = () => { Object.keys(formData).forEach((k) => { formMeta[k] = { ...formData[k], value: setting[k] }; }); + console.log(formMeta); setFormData({ ...formData, ...formMeta }); }, [setting]); const handleOnChange = (data) => { + console.table(data); setFormData(data); }; diff --git a/ui/src/stores/siteInfo.ts b/ui/src/stores/siteInfo.ts index 69469d2a..b7f31406 100644 --- a/ui/src/stores/siteInfo.ts +++ b/ui/src/stores/siteInfo.ts @@ -50,6 +50,7 @@ const siteInfo = create((set) => ({ short_description: '', site_url: '', contact_email: '', + check_update: true, permalink: 1, }, users: defaultUsersConf, From ea8ed68296134c17257e4acd60a811d16d8fecc6 Mon Sep 17 00:00:00 2001 From: hgaol Date: Fri, 8 Mar 2024 09:52:54 +0800 Subject: [PATCH 12/15] remove console log --- ui/src/pages/Admin/General/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 3b4d4ef3..1a049bd8 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -168,12 +168,10 @@ const General: FC = () => { Object.keys(formData).forEach((k) => { formMeta[k] = { ...formData[k], value: setting[k] }; }); - console.log(formMeta); setFormData({ ...formData, ...formMeta }); }, [setting]); const handleOnChange = (data) => { - console.table(data); setFormData(data); }; From c364e9684d54d40a37c4bceaa70568dcfae979e1 Mon Sep 17 00:00:00 2001 From: hgaol Date: Fri, 8 Mar 2024 17:16:08 +0800 Subject: [PATCH 13/15] add default to true in frontend --- ui/src/pages/Admin/General/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/src/pages/Admin/General/index.tsx b/ui/src/pages/Admin/General/index.tsx index 1a049bd8..3c6ff51a 100644 --- a/ui/src/pages/Admin/General/index.tsx +++ b/ui/src/pages/Admin/General/index.tsx @@ -72,6 +72,7 @@ const General: FC = () => { check_update: { type: 'boolean', title: t('check_update.label'), + default: true, }, }, }; From e529d33dbf451c9f45ac629551a8b6fa9412ea13 Mon Sep 17 00:00:00 2001 From: easy <58644520+foxzero-007@users.noreply.github.com> Date: Tue, 12 Mar 2024 17:12:35 +0800 Subject: [PATCH 14/15] fix(ui): fixed the ui issue of skeleton screen flickering (#830) The changes made in this pull request are quite extensive, and the proposed solution is briefly described in the following text. I hope you can take some time to review it when you have a moment. **What did I modify in this branch?** Perhaps you have noticed that we have been using the skeleton screen technology on the frontend pages. However, the control over it seems a bit simplistic. We currently only initiate the skeleton screen when entering the page and directly replace it with new nodes upon interface returns. As a result, there is a "skeleton screen flickering" issue during usage. In favorable network conditions, we often observe a quick flash of the skeleton screen. From a user experience perspective, I prefer the page not to change so rapidly. Therefore, I have submitted this pull request to address the issue. **How did I make the modification?** Through code comparison, it can be observed that I actually encapsulated a new hook named `useSkeletonControl`. The parameters it returns are as follows: ```ts { isSkeletonShow, // Should the skeleton screen be displayed? openSkeleton, // Activate the skeleton screen. closeSkeleton // Deactivate the skeleton screen. } ``` In the code logic, you can see that I have implemented a delay-like operation for both activation and deactivation. The reason behind this is that I believe the opening logic of the skeleton screen should be like this: - When the interface returns within 1 second, there is no need to activate the skeleton screen. - When the interface takes more than 1 second to return, the skeleton screen is activated, and the activation time should not be less than 3 seconds. (The above-mentioned times are all defined as constants and can be adjusted according to the actual situation.) Based on this design approach, I utilized `setTimeout` and `clearTimeout` to control the timing of activation and deactivation. Why provide a `needShowFirst` parameter? Clearly, this parameter is used to control whether the skeleton screen is rendered right from the start. It allows the skeleton screen to bypass the first timing condition mentioned earlier, meaning it can be activated even if the interface returns in less than 1 second. The reason behind this is that I noticed in some pages, the skeleton screen needs to occupy its position right from the start to prevent the 'flickering' issue on the page. So, I introduced this parameter. However, in my design, I don't intend to show the skeleton screen when the interface response time is too short, as it might negatively impact the user experience. The above is the reason behind my pull request. I have made modifications for the pages I identified, and I hope they will be accepted. Additionally, if there are better solutions, I am eager to see them. Thanks again for considering my changes. --------- Co-authored-by: LinkinStars Co-authored-by: robin --- ui/src/common/constants.ts | 1 + ui/src/components/QuestionList/index.tsx | 5 +- ui/src/hooks/index.ts | 2 + ui/src/hooks/useSkeletonControl/index.tsx | 61 +++++++++++++++++++++++ ui/src/pages/Questions/Detail/index.tsx | 5 +- ui/src/pages/Search/index.tsx | 5 +- ui/src/pages/Tags/index.tsx | 8 ++- 7 files changed, 80 insertions(+), 7 deletions(-) create mode 100644 ui/src/hooks/useSkeletonControl/index.tsx diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 8868c9a0..ec7f1ba6 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -30,6 +30,7 @@ export const DRAFT_TIMESIGH_STORAGE_KEY = '|_a_t_s_|'; export const QUESTIONS_ORDER_STORAGE_KEY = '_a_qok_'; export const DEFAULT_THEME = 'system'; export const ADMIN_PRIVILEGE_CUSTOM_LEVEL = 99; +export const SKELETON_SHOW_TIME = 1000; export const USER_AGENT_NAMES = { SegmentFault: 'SegmentFault', diff --git a/ui/src/components/QuestionList/index.tsx b/ui/src/components/QuestionList/index.tsx index 4f410990..94a300d8 100644 --- a/ui/src/components/QuestionList/index.tsx +++ b/ui/src/components/QuestionList/index.tsx @@ -36,6 +36,7 @@ import { Icon, } from '@/components'; import * as Type from '@/common/interface'; +import { useSkeletonControl } from '@/hooks'; export const QUESTION_ORDER_KEYS: Type.QuestionOrderBy[] = [ 'active', @@ -59,11 +60,13 @@ const QuestionList: FC = ({ }) => { const { t } = useTranslation('translation', { keyPrefix: 'question' }); const [urlSearchParams] = useSearchParams(); + const { isSkeletonShow } = useSkeletonControl(isLoading); const curOrder = order || urlSearchParams.get('order') || QUESTION_ORDER_KEYS[0]; const curPage = Number(urlSearchParams.get('page')) || 1; const pageSize = 20; const count = data?.count || 0; + return (
@@ -80,7 +83,7 @@ const QuestionList: FC = ({ />
- {isLoading ? ( + {isSkeletonShow ? ( ) : ( data?.list?.map((li) => { diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts index 54c7fd28..d0615fa3 100644 --- a/ui/src/hooks/index.ts +++ b/ui/src/hooks/index.ts @@ -29,6 +29,7 @@ import useLoginRedirect from './useLoginRedirect'; import usePromptWithUnload from './usePrompt'; import useActivationEmailModal from './useActivationEmailModal'; import useCaptchaModal from './useCaptchaModal'; +import useSkeletonControl from './useSkeletonControl'; export { useTagModal, @@ -43,4 +44,5 @@ export { usePromptWithUnload, useActivationEmailModal, useCaptchaModal, + useSkeletonControl, }; diff --git a/ui/src/hooks/useSkeletonControl/index.tsx b/ui/src/hooks/useSkeletonControl/index.tsx new file mode 100644 index 00000000..2642ba9f --- /dev/null +++ b/ui/src/hooks/useSkeletonControl/index.tsx @@ -0,0 +1,61 @@ +/* + * 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. + */ + +import { useEffect, useRef, useState } from 'react'; + +import { SKELETON_SHOW_TIME } from '@/common/constants'; + +/** + * @param needShowFirst whether the skeleton should show at first + * + * Why need 'needShowFirst' param? + * Sometimes we need skeleton screens to take up space in the dom from the start + * + * If you set the 'needShowFirst' param as false, If the interface time is too short, + * the skeleton screen will not be displayed, which can reduce the time occupation + */ +const useSkeletonControl = (isLoading: boolean) => { + const [isSkeletonShow, setIsSkeletonShow] = useState(false); + const timer = useRef(null); + const openSkeleton = () => { + if (timer.current) { + clearTimeout(timer.current); + } + timer.current = setTimeout(() => { + setIsSkeletonShow(true); + }, SKELETON_SHOW_TIME); + }; + + const closeSkeleton = () => { + clearTimeout(timer.current as NodeJS.Timeout); + setIsSkeletonShow(false); + }; + + useEffect(() => { + if (isLoading) { + openSkeleton(); + } else { + closeSkeleton(); + } + }, [isLoading]); + + return { isSkeletonShow }; +}; + +export default useSkeletonControl; diff --git a/ui/src/pages/Questions/Detail/index.tsx b/ui/src/pages/Questions/Detail/index.tsx index e0908eac..b289ac68 100644 --- a/ui/src/pages/Questions/Detail/index.tsx +++ b/ui/src/pages/Questions/Detail/index.tsx @@ -30,7 +30,7 @@ import { useTranslation } from 'react-i18next'; import { Pagination, CustomSidebar } from '@/components'; import { loggedUserInfoStore, toastStore } from '@/stores'; import { scrollToElementTop, scrollToDocTop } from '@/utils'; -import { usePageTags, usePageUsers } from '@/hooks'; +import { usePageTags, usePageUsers, useSkeletonControl } from '@/hooks'; import type { ListResult, QuestionDetailRes, @@ -68,6 +68,7 @@ const Index = () => { const order = urlSearch.get('order') || ''; const [question, setQuestion] = useState(null); const [isLoading, setIsLoading] = useState(true); + const { isSkeletonShow } = useSkeletonControl(isLoading); const [answers, setAnswers] = useState>({ count: -1, list: [], @@ -239,7 +240,7 @@ const Index = () => { {question?.operation?.level && } - {isLoading ? ( + {isSkeletonShow ? ( ) : ( { const q = searchParams.get('q') || ''; const order = searchParams.get('order') || 'active'; const [isLoading, setIsLoading] = useState(false); + const { isSkeletonShow } = useSkeletonControl(isLoading); const [data, setData] = useState({ count: 0, list: [], @@ -102,7 +103,7 @@ const Index = () => { - {isLoading ? ( + {isSkeletonShow ? ( ) : ( list?.map((item) => { diff --git a/ui/src/pages/Tags/index.tsx b/ui/src/pages/Tags/index.tsx index aa0f8c6e..e8f682f7 100644 --- a/ui/src/pages/Tags/index.tsx +++ b/ui/src/pages/Tags/index.tsx @@ -22,7 +22,7 @@ import { Row, Col, Card, Button, Form, Stack } from 'react-bootstrap'; import { useSearchParams, Link } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; -import { usePageTags } from '@/hooks'; +import { usePageTags, useSkeletonControl } from '@/hooks'; import { Tag, Pagination, QueryGroup, TagsLoader } from '@/components'; import { formatCount, escapeRemove } from '@/utils'; import { tryNormalLogged } from '@/utils/guard'; @@ -52,6 +52,8 @@ const Tags = () => { ...(sort ? { query_cond: sort } : {}), }); + const { isSkeletonShow } = useSkeletonControl(isLoading); + const handleChange = (e) => { setSearchTag(e.target.value); }; @@ -67,9 +69,11 @@ const Tags = () => { mutate(); }); }; + usePageTags({ title: t('tags', { keyPrefix: 'page_title' }), }); + return ( @@ -106,7 +110,7 @@ const Tags = () => { - {isLoading ? ( + {isSkeletonShow ? ( ) : ( tags?.list?.map((tag) => ( From f9a93e2efea182bcdc67abd635d9b081501ef06c Mon Sep 17 00:00:00 2001 From: hgaol Date: Wed, 13 Mar 2024 10:09:49 +0800 Subject: [PATCH 15/15] fix: not changing rank for newly registered user when changing mail --- internal/service/user_service.go | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/internal/service/user_service.go b/internal/service/user_service.go index c6d98fc2..bcac08ad 100644 --- a/internal/service/user_service.go +++ b/internal/service/user_service.go @@ -23,9 +23,10 @@ import ( "context" "encoding/json" "fmt" + "time" + "github.com/apache/incubator-answer/internal/base/constant" "github.com/apache/incubator-answer/internal/service/user_notification_config" - "time" "github.com/apache/incubator-answer/internal/base/handler" "github.com/apache/incubator-answer/internal/base/reason" @@ -644,6 +645,12 @@ func (us *UserService) UserChangeEmailVerify(ctx context.Context, content string if err != nil { return nil, err } + // if email status is to be verified, active user as well + if userInfo.MailStatus == entity.EmailStatusToBeVerified { + if err = us.userActivity.UserActive(ctx, userInfo.ID); err != nil { + log.Error(err) + } + } roleID, err := us.userRoleService.GetUserRole(ctx, userInfo.ID) if err != nil {