From 78832d3994cf6734610cc69fcf0983d944482887 Mon Sep 17 00:00:00 2001 From: Dinesht04 Date: Thu, 16 Oct 2025 23:06:22 +0530 Subject: [PATCH 1/3] feat(ui,internal): add min_content property in site info --- i18n/en_US.yaml | 5 ++++- internal/migrations/init.go | 1 + internal/migrations/v28.go | 2 ++ internal/schema/siteinfo_schema.go | 1 + ui/src/common/interface.ts | 1 + ui/src/pages/Admin/Write/index.tsx | 33 +++++++++++++++++++++++++----- ui/src/stores/writeSetting.ts | 1 + 7 files changed, 38 insertions(+), 6 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 146cd0af4..5f694ea28 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -2122,13 +2122,16 @@ ui: ask_before_display: Ask before displaying external content write: page_title: Write + min_content: + label: Minimum question body length + text: Minimum allowed question body length in characters. restrict_answer: title: Answer write label: Each user can only write one answer for the same question text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." min_tags: label: "Minimum tags per question" - text: "Minimum number of tags required in a question" + text: "Minimum number of tags required in a question." recommend_tags: label: Recommend tags text: "Recommend tags will show in the dropdown list by default." diff --git a/internal/migrations/init.go b/internal/migrations/init.go index a14a371dc..392ecb2c6 100644 --- a/internal/migrations/init.go +++ b/internal/migrations/init.go @@ -289,6 +289,7 @@ func (m *Mentor) initSiteInfoPrivilegeRank() { func (m *Mentor) initSiteInfoWrite() { writeData := map[string]interface{}{ + "min_content": 6, "restrict_answer": true, "min_tags": 1, "required_tag": false, diff --git a/internal/migrations/v28.go b/internal/migrations/v28.go index 8b834afad..0fa8ef884 100644 --- a/internal/migrations/v28.go +++ b/internal/migrations/v28.go @@ -41,6 +41,7 @@ func addOptionalTags(ctx context.Context, x *xorm.Engine) error { } if exist { type OldSiteWriteReq struct { + MinimumContent int `json:"min_content"` RestrictAnswer bool `json:"restrict_answer"` MinimumTags int `json:"min_tags"` RequiredTag bool `json:"required_tag"` @@ -55,6 +56,7 @@ func addOptionalTags(ctx context.Context, x *xorm.Engine) error { content := &OldSiteWriteReq{} _ = json.Unmarshal([]byte(writeSiteInfo.Content), content) content.MinimumTags = 1 + content.MinimumContent = 6 data, _ := json.Marshal(content) writeSiteInfo.Content = string(data) _, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo) diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index 1ecd9760d..d946589d8 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -75,6 +75,7 @@ type SiteBrandingReq struct { // SiteWriteReq site write request type SiteWriteReq struct { + MinimumContent int `validate:"omitempty" json:"min_content"` RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"` MinimumTags int `validate:"omitempty" json:"min_tags"` RequiredTag bool `validate:"omitempty" json:"required_tag"` diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index bade01ea0..b5a242e41 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -440,6 +440,7 @@ export interface AdminSettingsLegal { export interface AdminSettingsWrite { restrict_answer?: boolean; min_tags?: number, + min_content?:number, recommend_tags?: Tag[]; required_tag?: boolean; reserved_tags?: Tag[]; diff --git a/ui/src/pages/Admin/Write/index.tsx b/ui/src/pages/Admin/Write/index.tsx index 15c0c2a6f..86d44d6d9 100644 --- a/ui/src/pages/Admin/Write/index.tsx +++ b/ui/src/pages/Admin/Write/index.tsx @@ -37,6 +37,11 @@ const initFormData = { errorMsg: '', isInvalid: false, }, + min_content: { + value: 0, + errorMsg: '', + isInvalid: false, + }, min_tags: { value: 0, errorMsg: '', @@ -142,6 +147,7 @@ const Index: FC = () => { reserved_tags: formData.reserved_tags.value, required_tag: formData.required_tag.value, restrict_answer: formData.restrict_answer.value, + min_content: Number(formData.min_content.value), max_image_size: Number(formData.max_image_size.value), max_attachment_size: Number(formData.max_attachment_size.value), max_image_megapixel: Number(formData.max_image_megapixel.value), @@ -183,6 +189,7 @@ const Index: FC = () => { if (Array.isArray(res.recommend_tags)) { formData.recommend_tags.value = res.recommend_tags; } + formData.min_content.value = res.min_content; formData.min_tags.value = res.min_tags; formData.required_tag.value = res.required_tag; formData.restrict_answer.value = res.restrict_answer; @@ -204,10 +211,6 @@ const Index: FC = () => { initData(); }, []); - // const handleOnChange = (data) => { - // setFormData(data); - // }; - return ( <>

{t('page_title')}

@@ -295,7 +298,27 @@ const Index: FC = () => { {formData.required_tag.errorMsg} - + + {t('min_content.label')} + { + handleValueChange({ + min_content: { + value: evt.target.value, + errorMsg: '', + isInvalid: false, + }, + }); + }} + /> + {t('min_content.text')} + + {formData.min_content.errorMsg} + + {t('restrict_answer.title')} ((set) => ({ write: { restrict_answer: true, min_tags: 1, + min_content: 6, recommend_tags: [], required_tag: false, reserved_tags: [], From ae6ea882330476aaa6f40bcd663e99c5ab4a9caa Mon Sep 17 00:00:00 2001 From: Dinesht04 Date: Fri, 17 Oct 2025 16:48:22 +0530 Subject: [PATCH 2/3] feat(ui,internal): add optional question body --- i18n/en_US.yaml | 9 ++++- internal/base/reason/reason.go | 1 + internal/schema/question_schema.go | 26 ++----------- internal/service/content/question_service.go | 40 ++++++++++++++++++++ internal/service/question_common/question.go | 8 ++++ ui/src/pages/Questions/Ask/index.tsx | 26 ++++++++++--- 6 files changed, 80 insertions(+), 30 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 5f694ea28..91bfa2afc 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -235,6 +235,8 @@ backend: other: No permission to update. content_cannot_empty: other: Content cannot be empty. + content_less_than_minumum: + other: Not enough content entered. rank: fail_to_meet_the_condition: other: Reputation rank fail to meet the condition. @@ -1160,6 +1162,9 @@ ui: label: Body msg: empty: Body cannot be empty. + hint: + optional_body: Share what the question is about. + minimum_characters: "Share what the question is about, at least {{ min_content_length }} characters are required." tags: label: Tags msg: @@ -1181,8 +1186,8 @@ ui: add_btn: Add tag create_btn: Create new tag search_tag: Search tag - hint: "Describe what your content is about, at least one tag is required." - hint_zero_tags: " Describe what your content is about." + hint: Describe what your content is about, at least one tag is required. + hint_zero_tags: Describe what your content is about. hint_more_than_one_tag: "Describe what your content is about, at least {{ min_tags_number }} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) diff --git a/internal/base/reason/reason.go b/internal/base/reason/reason.go index 5a34d9218..42e29f4c5 100644 --- a/internal/base/reason/reason.go +++ b/internal/base/reason/reason.go @@ -47,6 +47,7 @@ const ( QuestionAlreadyDeleted = "error.question.already_deleted" QuestionUnderReview = "error.question.under_review" QuestionContentCannotEmpty = "error.question.content_cannot_empty" + QuestionContentLessThanMinimum = "error.question.content_less_than_minumum" AnswerNotFound = "error.answer.not_found" AnswerCannotDeleted = "error.answer.cannot_deleted" AnswerCannotUpdate = "error.answer.cannot_update" diff --git a/internal/schema/question_schema.go b/internal/schema/question_schema.go index 5092ddf82..84b97b830 100644 --- a/internal/schema/question_schema.go +++ b/internal/schema/question_schema.go @@ -79,7 +79,7 @@ type QuestionAdd struct { // question title Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content - Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + Content string `validate:"gte=0,lte=65535" json:"content"` // html HTML string `json:"-"` // tags @@ -100,12 +100,6 @@ func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err erro tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) } } - if req.HTML == "" { - return append(errFields, &validator.FormErrorField{ - ErrorField: "content", - ErrorMsg: reason.QuestionContentCannotEmpty, - }), errors.BadRequest(reason.QuestionContentCannotEmpty) - } return nil, nil } @@ -113,7 +107,7 @@ type QuestionAddByAnswer struct { // question title Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content - Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + Content string `validate:"gte=0,lte=65535" json:"content"` // html HTML string `json:"-"` AnswerContent string `validate:"required,notblank,gte=6,lte=65535" json:"answer_content"` @@ -138,19 +132,11 @@ func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField, tag.ParsedText = converter.Markdown2HTML(tag.OriginalText) } } - if req.HTML == "" { - errFields = append(errFields, &validator.FormErrorField{ - ErrorField: "content", - ErrorMsg: reason.QuestionContentCannotEmpty, - }) - } if req.AnswerHTML == "" { errFields = append(errFields, &validator.FormErrorField{ ErrorField: "answer_content", ErrorMsg: reason.AnswerContentCannotEmpty, }) - } - if req.HTML == "" || req.AnswerHTML == "" { return errFields, errors.BadRequest(reason.QuestionContentCannotEmpty) } return nil, nil @@ -195,7 +181,7 @@ type QuestionUpdate struct { // question title Title string `validate:"required,notblank,gte=6,lte=150" json:"title"` // content - Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"` + Content string `validate:"gte=0,lte=65535" json:"content"` // html HTML string `json:"-"` InviteUser []string `validate:"omitempty" json:"invite_user"` @@ -227,12 +213,6 @@ type QuestionUpdateInviteUser struct { func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) { req.HTML = converter.Markdown2HTML(req.Content) - if req.HTML == "" { - return append(errFields, &validator.FormErrorField{ - ErrorField: "content", - ErrorMsg: reason.QuestionContentCannotEmpty, - }), errors.BadRequest(reason.QuestionContentCannotEmpty) - } return nil, nil } diff --git a/internal/service/content/question_service.go b/internal/service/content/question_service.go index 6832437da..1d1d1af39 100644 --- a/internal/service/content/question_service.go +++ b/internal/service/content/question_service.go @@ -242,6 +242,19 @@ func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.Que err = errors.BadRequest(reason.TagMinCount) return errorlist, err } + minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx) + if err != nil { + return + } + if len(req.Content) < minimumContentLength { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum), + }) + err = errors.BadRequest(reason.QuestionContentLessThanMinimum) + return errorlist, err + } recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) if err != nil { return @@ -301,6 +314,19 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question err = errors.BadRequest(reason.TagMinCount) return errorlist, err } + minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx) + if err != nil { + return + } + if len(req.Content) < minimumContentLength { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum), + }) + err = errors.BadRequest(reason.QuestionContentLessThanMinimum) + return errorlist, err + } recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags) if err != nil { return @@ -907,6 +933,20 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest question.UserID = dbinfo.UserID question.LastEditUserID = req.UserID + minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx) + if err != nil { + return + } + if len(req.Content) < minimumContentLength { + errorlist := make([]*validator.FormErrorField, 0) + errorlist = append(errorlist, &validator.FormErrorField{ + ErrorField: "content", + ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum), + }) + err = errors.BadRequest(reason.QuestionContentLessThanMinimum) + return errorlist, err + } + oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID) if tagerr != nil { return questionInfo, tagerr diff --git a/internal/service/question_common/question.go b/internal/service/question_common/question.go index 923920485..feb5626ed 100644 --- a/internal/service/question_common/question.go +++ b/internal/service/question_common/question.go @@ -899,3 +899,11 @@ func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMs questionID = uid.DeShortID(questionID) return questionID } + +func (qs *QuestionCommon) GetMinimumContentLength(ctx context.Context) (int, error) { + siteInfo, err := qs.siteInfoService.GetSiteWrite(ctx) + if err != nil { + return 6, err + } + return siteInfo.MinimumContent, nil +} diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index 149373ed9..d78041952 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -28,6 +28,7 @@ import isEqual from 'lodash/isEqual'; import debounce from 'lodash/debounce'; import fm from 'front-matter'; +import { writeSettingStore } from '@/stores'; import { usePageTags, usePromptWithUnload } from '@/hooks'; import { Editor, EditorRef, TagSelector } from '@/components'; import type * as Type from '@/common/interface'; @@ -120,6 +121,7 @@ const Ask = () => { handleTagsChange(resp); }); }; + const writeInfo = writeSettingStore((state) => state.write); const isEdit = qid !== undefined; @@ -423,6 +425,24 @@ const Ask = () => { usePageTags({ title: pageTitle, }); + + const handleContentHint = () => { + if ( + !writeInfo || + writeInfo.min_content === undefined || + !writeInfo.min_content + ) { + return t(`form.fields.body.hint.optional_body`); + } + + let str: string = t(`form.fields.body.hint.minimum_characters`); + str = str.replace( + `{{ min_content_length }}`, + writeInfo.min_content.toString(), + ); + return str; + }; + return (

{isEdit ? t('edit_title') : t('title')}

@@ -451,7 +471,6 @@ const Ask = () => { )} - {t('form.fields.title.label')} { {bool && } - {t('form.fields.body.label')} { }} ref={editorRef} /> + {handleContentHint()} {formData.content.errorMsg} - {t('form.fields.tags.label')} { errMsg={formData.tags.errorMsg} /> - {!isEdit && ( <> { )} )} - {isEdit && ( {t('form.fields.edit_summary.label')} From 6c30c38fd9b6b29b7b1335d264c3ba1772e9daa9 Mon Sep 17 00:00:00 2001 From: Dinesht04 Date: Tue, 21 Oct 2025 16:24:25 +0530 Subject: [PATCH 3/3] fix(ui,internal): correct i18n implementation for variables and add min=0 to minimumContent in siteInfo schema --- i18n/en_US.yaml | 4 ++-- internal/schema/siteinfo_schema.go | 2 +- ui/src/components/TagSelector/index.tsx | 6 +++--- ui/src/pages/Questions/Ask/index.tsx | 9 +++------ 4 files changed, 9 insertions(+), 12 deletions(-) diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 91bfa2afc..17038d074 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1164,7 +1164,7 @@ ui: empty: Body cannot be empty. hint: optional_body: Share what the question is about. - minimum_characters: "Share what the question is about, at least {{ min_content_length }} characters are required." + minimum_characters: "Share what the question is about, at least {{min_content_length}} characters are required." tags: label: Tags msg: @@ -1188,7 +1188,7 @@ ui: search_tag: Search tag hint: Describe what your content is about, at least one tag is required. hint_zero_tags: Describe what your content is about. - hint_more_than_one_tag: "Describe what your content is about, at least {{ min_tags_number }} tags are required." + hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required." no_result: No tags matched tag_required_text: Required tag (at least one) header: diff --git a/internal/schema/siteinfo_schema.go b/internal/schema/siteinfo_schema.go index d946589d8..15acb6d64 100644 --- a/internal/schema/siteinfo_schema.go +++ b/internal/schema/siteinfo_schema.go @@ -75,7 +75,7 @@ type SiteBrandingReq struct { // SiteWriteReq site write request type SiteWriteReq struct { - MinimumContent int `validate:"omitempty" json:"min_content"` + MinimumContent int `validate:"omitempty,min=0" json:"min_content"` RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"` MinimumTags int `validate:"omitempty" json:"min_tags"` RequiredTag bool `validate:"omitempty" json:"required_tag"` diff --git a/ui/src/components/TagSelector/index.tsx b/ui/src/components/TagSelector/index.tsx index 94e953259..4419d359a 100644 --- a/ui/src/components/TagSelector/index.tsx +++ b/ui/src/components/TagSelector/index.tsx @@ -304,9 +304,9 @@ const TagSelector: FC = ({ return t(`hint`); } - let str: string = t(`hint_more_than_one_tag`); - str = str.replace(`{{ min_tags_number }}`, writeInfo.min_tags.toString()); - return str; + return t(`hint_more_than_one_tag`, { + min_tags_number: writeInfo.min_tags, + }); }; useEffect(() => { diff --git a/ui/src/pages/Questions/Ask/index.tsx b/ui/src/pages/Questions/Ask/index.tsx index d78041952..cac75dd87 100644 --- a/ui/src/pages/Questions/Ask/index.tsx +++ b/ui/src/pages/Questions/Ask/index.tsx @@ -435,12 +435,9 @@ const Ask = () => { return t(`form.fields.body.hint.optional_body`); } - let str: string = t(`form.fields.body.hint.minimum_characters`); - str = str.replace( - `{{ min_content_length }}`, - writeInfo.min_content.toString(), - ); - return str; + return t(`form.fields.body.hint.minimum_characters`, { + min_content_length: writeInfo.min_content, + }); }; return (