Merge branch 'github-main' into beta/1.1.0

# Conflicts:
#	Makefile
#	internal/migrations/migrations.go
#	ui/src/common/interface.ts
This commit is contained in:
LinkinStars 2023-04-19 16:57:48 +08:00
commit fc9de356d6
34 changed files with 724 additions and 68 deletions

View File

@ -3512,6 +3512,45 @@ const docTemplate = `{
}
}
},
"/answer/api/v1/question/operation": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Operation question \\n operation [pin unpin hide show]",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "Operation question",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.OperationQuestionReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/page": {
"get": {
"description": "get questions by page",
@ -7275,6 +7314,21 @@ const docTemplate = `{
}
}
},
"schema.OperationQuestionReq": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"operation": {
"description": "operation [pin unpin hide show]",
"type": "string"
}
}
},
"schema.PermissionMemberAction": {
"type": "object",
"properties": {
@ -7430,6 +7484,14 @@ const docTemplate = `{
"operator": {
"$ref": "#/definitions/schema.QuestionPageRespOperator"
},
"pin": {
"description": "1: unpin, 2: pin",
"type": "integer"
},
"show": {
"description": "0: show, 1: hide",
"type": "integer"
},
"status": {
"type": "integer"
},

View File

@ -3500,6 +3500,45 @@
}
}
},
"/answer/api/v1/question/operation": {
"put": {
"security": [
{
"ApiKeyAuth": []
}
],
"description": "Operation question \\n operation [pin unpin hide show]",
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"tags": [
"Question"
],
"summary": "Operation question",
"parameters": [
{
"description": "question",
"name": "data",
"in": "body",
"required": true,
"schema": {
"$ref": "#/definitions/schema.OperationQuestionReq"
}
}
],
"responses": {
"200": {
"description": "OK",
"schema": {
"$ref": "#/definitions/handler.RespBody"
}
}
}
}
},
"/answer/api/v1/question/page": {
"get": {
"description": "get questions by page",
@ -7263,6 +7302,21 @@
}
}
},
"schema.OperationQuestionReq": {
"type": "object",
"required": [
"id"
],
"properties": {
"id": {
"type": "string"
},
"operation": {
"description": "operation [pin unpin hide show]",
"type": "string"
}
}
},
"schema.PermissionMemberAction": {
"type": "object",
"properties": {
@ -7418,6 +7472,14 @@
"operator": {
"$ref": "#/definitions/schema.QuestionPageRespOperator"
},
"pin": {
"description": "1: unpin, 2: pin",
"type": "integer"
},
"show": {
"description": "0: show, 1: hide",
"type": "integer"
},
"status": {
"type": "integer"
},

View File

@ -1112,6 +1112,16 @@ definitions:
description: inbox achievement
type: string
type: object
schema.OperationQuestionReq:
properties:
id:
type: string
operation:
description: operation [pin unpin hide show]
type: string
required:
- id
type: object
schema.PermissionMemberAction:
properties:
action:
@ -1224,6 +1234,12 @@ definitions:
type: string
operator:
$ref: '#/definitions/schema.QuestionPageRespOperator'
pin:
description: '1: unpin, 2: pin'
type: integer
show:
description: '0: show, 1: hide'
type: integer
status:
type: integer
tags:
@ -4243,6 +4259,30 @@ paths:
summary: get question details
tags:
- Question
/answer/api/v1/question/operation:
put:
consumes:
- application/json
description: Operation question \n operation [pin unpin hide show]
parameters:
- description: question
in: body
name: data
required: true
schema:
$ref: '#/definitions/schema.OperationQuestionReq'
produces:
- application/json
responses:
"200":
description: OK
schema:
$ref: '#/definitions/handler.RespBody'
security:
- ApiKeyAuth: []
summary: Operation question
tags:
- Question
/answer/api/v1/question/page:
get:
consumes:

View File

@ -23,6 +23,14 @@ backend:
other: Close
reopen:
other: Reopen
pin:
other: Pin
hide:
other: Unlist
unpin:
other: Unpin
show:
other: List
role:
name:
user:
@ -809,6 +817,7 @@ ui:
btn: Add question
answers: answers
question_detail:
action: Action
Asked: Asked
asked: asked
update: Modified
@ -847,13 +856,14 @@ ui:
li1_2: Back up any statements you make with references or personal experience.
header_2: But <strong>avoid</strong> ...
li2_1: Asking for help, seeking clarification, or responding to other answers.
reopen:
confirm_btn: Reopen
title: Reopen this post
content: Are you sure you want to reopen?
success: This post has been reopened
pin:
title: Pin this post
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
confirm_btn: Pin
delete:
title: Delete this post
question: >-
@ -867,7 +877,6 @@ ui:
of accepted answers can result in your account being blocked from answering.
Are you sure you wish to delete?
other: Are you sure you wish to delete?
tip_question_deleted: This post has been deleted
tip_answer_deleted: This answer has been deleted
btns:
confirm: Confirm
@ -883,6 +892,7 @@ ui:
reject: Reject
skip: Skip
discard_draft: Discard draft
pinned: Pinned
search:
title: Search Results
keywords: Keywords
@ -1482,6 +1492,10 @@ ui:
closed: closed
reopened: reopened
created: created
pin: pinned
unpin: unpinned
show: listed
hide: unlisted
title: "History for"
tag_title: "Timeline for"
show_votes: "Show votes"
@ -1507,5 +1521,9 @@ ui:
draft:
discard_confirm: Are you sure you want to discard your draft?
messages:
post_deleted: This post has been deleted.
post_deleted: This post has been deleted.
post_pin: This post has been pinned.
post_unpin: This post has been unpinned.
post_hide_list: This post has been hidden from list.
post_show_list: This post has been shown to list.
post_reopen: This post has been reopened.

View File

@ -22,6 +22,14 @@ backend:
other: 关闭
reopen:
other: 重新打开
pin:
other: 置顶
hide:
other: 隐藏
unpin:
other: 取消置顶
show:
other: 显示
role:
name:
user:

View File

@ -14,6 +14,10 @@ const (
ActFollow = "follow"
ActAccepted = "accepted"
ActAccept = "accept"
ActPin = "pin"
ActUnPin = "unpin"
ActShow = "show"
ActHide = "hide"
)
const (
@ -29,6 +33,10 @@ const (
ActQuestionRollback ActivityTypeKey = "question.rollback"
ActQuestionDeleted ActivityTypeKey = "question.deleted"
ActQuestionUndeleted ActivityTypeKey = "question.undeleted"
ActQuestionPin ActivityTypeKey = "question.pin"
ActQuestionUnPin ActivityTypeKey = "question.unpin"
ActQuestionHide ActivityTypeKey = "question.hide"
ActQuestionShow ActivityTypeKey = "question.show"
)
const (

View File

@ -70,6 +70,47 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
handler.HandleResponse(ctx, err, nil)
}
// OperationQuestion Operation question
// @Summary Operation question
// @Description Operation question \n operation [pin unpin hide show]
// @Tags Question
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.OperationQuestionReq true "question"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/question/operation [put]
func (qc *QuestionController) OperationQuestion(ctx *gin.Context) {
req := &schema.OperationQuestionReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.ID = uid.DeShortID(req.ID)
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
canList, err := qc.rankService.CheckOperationPermissions(ctx, req.UserID, []string{
permission.QuestionPin,
permission.QuestionUnPin,
permission.QuestionHide,
permission.QuestionShow,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
return
}
req.CanPin = canList[0]
req.CanList = canList[1]
if (req.Operation == schema.QuestionOperationPin || req.Operation == schema.QuestionOperationUnPin) && !req.CanPin {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
if (req.Operation == schema.QuestionOperationHide || req.Operation == schema.QuestionOperationShow) && !req.CanList {
handler.HandleResponse(ctx, errors.Forbidden(reason.RankFailToMeetTheCondition), nil)
return
}
err = qc.questionService.OperationQuestion(ctx, req)
handler.HandleResponse(ctx, err, nil)
}
// CloseQuestion Close question
// @Summary Close question
// @Description Close question
@ -152,6 +193,10 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
permission.QuestionDelete,
permission.QuestionClose,
permission.QuestionReopen,
permission.QuestionPin,
permission.QuestionUnPin,
permission.QuestionHide,
permission.QuestionShow,
})
if err != nil {
handler.HandleResponse(ctx, err, nil)
@ -163,6 +208,10 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
req.CanDelete = canList[1]
req.CanClose = canList[2]
req.CanReopen = canList[3]
req.CanPin = canList[4]
req.CanUnPin = canList[5]
req.CanHide = canList[6]
req.CanShow = canList[7]
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
if err != nil {

View File

@ -8,6 +8,10 @@ const (
QuestionStatusAvailable = 1
QuestionStatusClosed = 2
QuestionStatusDeleted = 10
QuestionUnPin = 1
QuestionPin = 2
QuestionShow = 1
QuestionHide = 2
)
var AdminQuestionSearchStatus = map[string]int{
@ -32,6 +36,8 @@ type Question struct {
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
Status int `xorm:"not null default 1 INT(11) status"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`

View File

@ -56,6 +56,7 @@ var migrations = []Migration{
NewMigration("add user role", addRoleFeatures, false),
NewMigration("add theme and private mode", addThemeAndPrivateMode, true),
NewMigration("add new answer notification", addNewAnswerNotification, true),
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
NewMigration("add plugin", addPlugin, false),
}

118
internal/migrations/v8.go Normal file
View File

@ -0,0 +1,118 @@
package migrations
import (
"fmt"
"time"
"github.com/answerdev/answer/internal/entity"
"github.com/answerdev/answer/internal/service/permission"
"github.com/segmentfault/pacman/log"
"xorm.io/xorm"
)
func addRolePinAndHideFeatures(x *xorm.Engine) error {
powers := []*entity.Power{
{ID: 34, Name: "question pin", PowerType: permission.QuestionPin, Description: "top the question"},
{ID: 35, Name: "question hide", PowerType: permission.QuestionHide, Description: "hide the question"},
{ID: 36, Name: "question unpin", PowerType: permission.QuestionUnPin, Description: "untop the question"},
{ID: 37, Name: "question show", PowerType: permission.QuestionShow, Description: "show the question"},
}
// insert default powers
for _, power := range powers {
exist, err := x.Get(&entity.Power{ID: power.ID})
if err != nil {
return err
}
if exist {
_, err = x.ID(power.ID).Update(power)
} else {
_, err = x.Insert(power)
}
if err != nil {
return err
}
}
rolePowerRels := []*entity.RolePowerRel{
{RoleID: 2, PowerType: permission.QuestionPin},
{RoleID: 2, PowerType: permission.QuestionHide},
{RoleID: 2, PowerType: permission.QuestionUnPin},
{RoleID: 2, PowerType: permission.QuestionShow},
{RoleID: 3, PowerType: permission.QuestionPin},
{RoleID: 3, PowerType: permission.QuestionHide},
{RoleID: 3, PowerType: permission.QuestionUnPin},
{RoleID: 3, PowerType: permission.QuestionShow},
}
// insert default powers
for _, rel := range rolePowerRels {
exist, err := x.Get(&entity.RolePowerRel{RoleID: rel.RoleID, PowerType: rel.PowerType})
if err != nil {
return err
}
if exist {
continue
}
_, err = x.Insert(rel)
if err != nil {
return err
}
}
defaultConfigTable := []*entity.Config{
{ID: 119, Key: "question.pin", Value: `0`},
{ID: 120, Key: "question.unpin", Value: `0`},
{ID: 121, Key: "question.show", Value: `0`},
{ID: 122, Key: "question.hide", Value: `0`},
}
for _, c := range defaultConfigTable {
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
if err != nil {
return fmt.Errorf("get config failed: %w", err)
}
if exist {
if _, err = x.Update(c, &entity.Config{ID: c.ID, Key: c.Key}); err != nil {
log.Errorf("update %+v config failed: %s", c, err)
return fmt.Errorf("update config failed: %w", err)
}
continue
}
if _, err = x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
log.Errorf("insert %+v config failed: %s", c, err)
return fmt.Errorf("add config failed: %w", err)
}
}
type Question struct {
ID string `xorm:"not null pk BIGINT(20) id"`
CreatedAt time.Time `xorm:"not null default CURRENT_TIMESTAMP TIMESTAMP created_at"`
UpdatedAt time.Time `xorm:"updated_at TIMESTAMP"`
UserID string `xorm:"not null default 0 BIGINT(20) INDEX user_id"`
LastEditUserID string `xorm:"not null default 0 BIGINT(20) last_edit_user_id"`
Title string `xorm:"not null default '' VARCHAR(150) title"`
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
ParsedText string `xorm:"not null MEDIUMTEXT parsed_text"`
Status int `xorm:"not null default 1 INT(11) status"`
Pin int `xorm:"not null default 1 INT(11) pin"`
Show int `xorm:"not null default 1 INT(11) show"`
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_view_count"`
VoteCount int `xorm:"not null default 0 INT(11) vote_count"`
AnswerCount int `xorm:"not null default 0 INT(11) answer_count"`
CollectionCount int `xorm:"not null default 0 INT(11) collection_count"`
FollowCount int `xorm:"not null default 0 INT(11) follow_count"`
AcceptedAnswerID string `xorm:"not null default 0 BIGINT(20) accepted_answer_id"`
LastAnswerID string `xorm:"not null default 0 BIGINT(20) last_answer_id"`
PostUpdateTime time.Time `xorm:"post_update_time TIMESTAMP"`
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
}
err := x.Sync(new(Question))
if err != nil {
return err
}
return nil
}

View File

@ -125,6 +125,15 @@ func (qr *questionRepo) UpdateQuestionStatusWithOutUpdateTime(ctx context.Contex
return nil
}
func (qr *questionRepo) UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Where("id =?", question.ID).Cols("pin", "show").Update(question)
if err != nil {
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
}
return nil
}
func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) {
question.ID = uid.DeShortID(question.ID)
_, err = qr.data.DB.Where("id =?", question.ID).Cols("accepted_answer_id").Update(question)
@ -224,6 +233,7 @@ func (qr *questionRepo) GetQuestionIDsPage(ctx context.Context, page, pageSize i
offset := page * pageSize
session := qr.data.DB.Table("question")
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
session.And("question.show = ?", entity.QuestionShow)
session = session.Limit(pageSize, offset)
session = session.OrderBy("question.created_at asc")
err = session.Select("id,title,created_at,post_update_time").Find(&rows)
@ -258,19 +268,22 @@ func (qr *questionRepo) GetQuestionPage(ctx context.Context, page, pageSize int,
}
if len(userID) > 0 {
session.And("question.user_id = ?", userID)
} else {
session.And("question.show = ?", entity.QuestionShow)
}
switch orderCond {
case "newest":
session.OrderBy("question.created_at DESC")
session.OrderBy("question.pin desc,question.created_at DESC")
case "active":
session.OrderBy("question.post_update_time DESC, question.updated_at DESC")
session.OrderBy("question.pin desc,question.post_update_time DESC, question.updated_at DESC")
case "frequent":
session.OrderBy("question.view_count DESC")
session.OrderBy("question.pin desc,question.view_count DESC")
case "score":
session.OrderBy("question.vote_count DESC, question.view_count DESC")
session.OrderBy("question.pin desc,question.vote_count DESC, question.view_count DESC")
case "unanswered":
session.Where("question.last_answer_id = 0")
session.OrderBy("question.created_at DESC")
session.OrderBy("question.pin desc,question.created_at DESC")
}
total, err = pager.Help(page, pageSize, &questionList, &entity.Question{}, session)

View File

@ -94,12 +94,14 @@ func (sr *searchRepo) SearchContents(ctx context.Context, words []string, tagIDs
ub = builder.MySQL().Select(afs...).From("`answer`").
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
ub.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).
And(builder.Eq{"`question`.`show`": entity.QuestionShow})
argsQ = append(argsQ, entity.QuestionStatusDeleted)
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow)
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
for i, word := range words {
if i == 0 {
@ -228,8 +230,8 @@ func (sr *searchRepo) SearchQuestions(ctx context.Context, words []string, tagID
b := builder.MySQL().Select(qfs...).From("question")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
args = append(args, entity.QuestionStatusDeleted)
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow)
for i, word := range words {
if i == 0 {
@ -343,8 +345,8 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
LeftJoin("`question`", "`question`.id = `answer`.question_id")
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
for i, word := range words {
if i == 0 {

View File

@ -193,6 +193,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.PUT("/question", a.questionController.UpdateQuestion)
r.DELETE("/question", a.questionController.RemoveQuestion)
r.PUT("/question/status", a.questionController.CloseQuestion)
r.PUT("/question/operation", a.questionController.OperationQuestion)
r.PUT("/question/reopen", a.questionController.ReopenQuestion)
r.GET("/question/similar", a.questionController.SearchByTitleLike)

View File

@ -8,9 +8,13 @@ import (
)
const (
SitemapMaxSize = 50000
SitemapCachekey = "answer@sitemap"
SitemapPageCachekey = "answer@sitemap@page%d"
SitemapMaxSize = 50000
SitemapCachekey = "answer@sitemap"
SitemapPageCachekey = "answer@sitemap@page%d"
QuestionOperationPin = "pin"
QuestionOperationUnPin = "unpin"
QuestionOperationHide = "hide"
QuestionOperationShow = "show"
)
// RemoveQuestionReq delete question request
@ -28,6 +32,14 @@ type CloseQuestionReq struct {
UserID string `json:"-"` // user_id
}
type OperationQuestionReq struct {
ID string `validate:"required" json:"id"`
Operation string `json:"operation"` // operation [pin unpin hide show]
UserID string `json:"-"` // user_id
CanPin bool `json:"-"`
CanList bool `json:"-"`
}
type CloseQuestionMeta struct {
CloseType int `json:"close_type"`
CloseMsg string `json:"close_msg"`
@ -101,6 +113,12 @@ type QuestionPermission struct {
CanClose bool `json:"-"`
// whether user can reopen it
CanReopen bool `json:"-"`
// whether user can pin it
CanPin bool `json:"-"`
CanUnPin bool `json:"-"`
// whether user can hide it
CanHide bool `json:"-"`
CanShow bool `json:"-"`
// whether user can use reserved it
CanUseReservedTag bool `json:"-"`
}
@ -168,6 +186,8 @@ type QuestionInfo struct {
UpdateTime int64 `json:"-"` // update_time
PostUpdateTime int64 `json:"update_time"`
QuestionUpdateTime int64 `json:"edit_time"`
Pin int `json:"pin"` // 1: unpin, 2: pin
Show int `json:"show"` // 0: show, 1: hide
Status int `json:"status"`
Operation *Operation `json:"operation,omitempty"`
UserID string `json:"-" `
@ -295,6 +315,8 @@ type QuestionPageResp struct {
Title string `json:"title"`
UrlTitle string `json:"url_title"`
Description string `json:"description"`
Pin int `json:"pin"` // 1: unpin, 2: pin
Show int `json:"show"` // 0: show, 1: hide
Status int `json:"status"`
Tags []*TagResp `json:"tags"`

View File

@ -10,6 +10,10 @@ const (
QuestionReopen = "question.reopen"
QuestionVoteUp = "question.vote_up"
QuestionVoteDown = "question.vote_down"
QuestionPin = "question.pin" //Top the question
QuestionUnPin = "question.unpin" //untop the question
QuestionHide = "question.hide" //hide the question
QuestionShow = "question.show" //show the question
AnswerAdd = "answer.add"
AnswerEdit = "answer.edit"
AnswerEditWithoutReview = "answer.edit_without_review"
@ -43,4 +47,8 @@ const (
deleteActionName = "action.delete"
closeActionName = "action.close"
reopenActionName = "action.reopen"
pinActionName = "action.pin"
unpinActionName = "action.unpin"
hideActionName = "action.hide"
showActionName = "action.show"
)

View File

@ -10,7 +10,7 @@ import (
// GetQuestionPermission get question permission
func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string,
canEdit, canDelete, canClose, canReopen bool) (
canEdit, canDelete, canClose, canReopen, canPin, canHide, CanUnPin, canShow bool) (
actions []*schema.PermissionMemberAction) {
lang := handler.GetLangByCtx(ctx)
actions = make([]*schema.PermissionMemberAction, 0)
@ -42,6 +42,36 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str
Type: "confirm",
})
}
if canPin {
actions = append(actions, &schema.PermissionMemberAction{
Action: "pin",
Name: translator.Tr(lang, pinActionName),
Type: "confirm",
})
}
if canHide {
actions = append(actions, &schema.PermissionMemberAction{
Action: "hide",
Name: translator.Tr(lang, hideActionName),
Type: "confirm",
})
}
if CanUnPin {
actions = append(actions, &schema.PermissionMemberAction{
Action: "unpin",
Name: translator.Tr(lang, unpinActionName),
Type: "confirm",
})
}
if canShow {
actions = append(actions, &schema.PermissionMemberAction{
Action: "show",
Name: translator.Tr(lang, showActionName),
Type: "confirm",
})
}
if canDelete || userID == creatorUserID {
actions = append(actions, &schema.PermissionMemberAction{
Action: "delete",

View File

@ -36,6 +36,7 @@ type QuestionRepo interface {
questionList []*entity.Question, total int64, err error)
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
UpdateQuestionStatusWithOutUpdateTime(ctx context.Context, question *entity.Question) (err error)
UpdateQuestionOperation(ctx context.Context, question *entity.Question) (err error)
SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error)
UpdatePvCount(ctx context.Context, questionID string) (err error)
UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error)
@ -271,6 +272,8 @@ func (qs *QuestionCommon) FormatQuestionsPage(
FollowCount: questionInfo.FollowCount,
AcceptedAnswerID: questionInfo.AcceptedAnswerID,
LastAnswerID: questionInfo.LastAnswerID,
Pin: questionInfo.Pin,
Show: questionInfo.Show,
}
questionIDs = append(questionIDs, questionInfo.ID)
@ -526,6 +529,8 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question)
info.QuestionUpdateTime = 0
}
info.Status = data.Status
info.Pin = data.Pin
info.Show = data.Show
info.UserID = data.UserID
info.LastEditUserID = data.LastEditUserID
if data.LastAnswerID != "0" {

View File

@ -270,6 +270,8 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
question.Status = entity.QuestionStatusAvailable
question.RevisionID = "0"
question.CreatedAt = now
question.Pin = entity.QuestionUnPin
question.Show = entity.QuestionShow
//question.UpdatedAt = nil
err = qs.questionRepo.AddQuestion(ctx, question)
if err != nil {
@ -319,6 +321,58 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
return
}
// OperationQuestion
func (qs *QuestionService) OperationQuestion(ctx context.Context, req *schema.OperationQuestionReq) (err error) {
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
if err != nil {
return err
}
if !has {
return nil
}
// Hidden question cannot be placed at the top
if questionInfo.Show == entity.QuestionHide && req.Operation == schema.QuestionOperationPin {
return nil
}
// Question cannot be hidden when they are at the top
if questionInfo.Pin == entity.QuestionPin && req.Operation == schema.QuestionOperationHide {
return nil
}
switch req.Operation {
case schema.QuestionOperationHide:
questionInfo.Show = entity.QuestionHide
case schema.QuestionOperationShow:
questionInfo.Show = entity.QuestionShow
case schema.QuestionOperationPin:
questionInfo.Pin = entity.QuestionPin
case schema.QuestionOperationUnPin:
questionInfo.Pin = entity.QuestionUnPin
}
err = qs.questionRepo.UpdateQuestionOperation(ctx, questionInfo)
if err != nil {
return err
}
actMap := make(map[string]constant.ActivityTypeKey)
actMap[schema.QuestionOperationPin] = constant.ActQuestionPin
actMap[schema.QuestionOperationUnPin] = constant.ActQuestionUnPin
actMap[schema.QuestionOperationHide] = constant.ActQuestionHide
actMap[schema.QuestionOperationShow] = constant.ActQuestionShow
_, ok := actMap[req.Operation]
if ok {
activity_queue.AddActivity(&schema.ActivityMsg{
UserID: req.UserID,
ObjectID: questionInfo.ID,
OriginalObjectID: questionInfo.ID,
ActivityTypeKey: actMap[req.Operation],
})
}
return nil
}
// RemoveQuestion delete question
func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
@ -632,6 +686,21 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
if question.Status == entity.QuestionStatusClosed {
per.CanClose = false
}
if question.Pin == entity.QuestionPin {
per.CanPin = false
per.CanHide = false
}
if question.Pin == entity.QuestionUnPin {
per.CanUnPin = false
}
if question.Show == entity.QuestionShow {
per.CanShow = false
}
if question.Show == entity.QuestionHide {
per.CanHide = false
per.CanPin = false
}
if question.Status == entity.QuestionStatusDeleted {
operation := &schema.Operation{}
operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted)
@ -641,7 +710,7 @@ func (qs *QuestionService) GetQuestion(ctx context.Context, questionID, userID s
question.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen)
per.CanEdit, per.CanDelete, per.CanClose, per.CanReopen, per.CanPin, per.CanHide, per.CanUnPin, per.CanShow)
return question, nil
}
@ -735,14 +804,15 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
if ok {
item.QuestionInfo = questionMaps[item.QuestionID]
}
}
for _, item := range answerlist {
info := &schema.UserAnswerInfo{}
_ = copier.Copy(info, item)
info.AnswerID = item.ID
info.QuestionID = item.QuestionID
userAnswerlist = append(userAnswerlist, info)
if item.QuestionInfo.Status != entity.QuestionStatusDeleted {
userAnswerlist = append(userAnswerlist, info)
}
}
return userAnswerlist, count, nil
}

View File

@ -1,6 +1,6 @@
const {
addWebpackModuleRule,
addWebpackAlias
addWebpackAlias,
} = require("customize-cra");
const path = require("path");

View File

@ -82,7 +82,7 @@
"react-app-rewired": "^2.2.1",
"react-scripts": "5.0.1",
"sass": "^1.54.4",
"typescript": "^4.9.5",
"typescript": "^4.8.3",
"yaml-loader": "^0.8.0"
},
"packageManager": "pnpm@7.9.5",

View File

@ -66,7 +66,7 @@ specifiers:
sass: ^1.54.4
semver: ^7.3.8
swr: ^1.3.0
typescript: ^4.9.5
typescript: ^4.8.3
urlcat: ^3.0.0
yaml-loader: ^0.8.0
zustand: ^4.1.1
@ -2077,7 +2077,7 @@ packages:
collect-v8-coverage: 1.0.1
exit: 0.1.2
glob: 7.2.3
graceful-fs: 4.2.10
graceful-fs: 4.2.11
istanbul-lib-coverage: 3.2.0
istanbul-lib-instrument: 5.2.0
istanbul-lib-report: 3.0.0
@ -2106,7 +2106,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
callsites: 3.1.0
graceful-fs: 4.2.10
graceful-fs: 4.2.11
source-map: 0.6.1
/@jest/test-result/27.5.1:
@ -2132,7 +2132,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@jest/test-result': 27.5.1
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jest-haste-map: 27.5.1
jest-runtime: 27.5.1
transitivePeerDependencies:
@ -5129,7 +5129,7 @@ packages:
resolution: {integrity: sha512-T0yTFjdpldGY8PmuXXR0PyQ1ufZpEGiHVrp7zHKB7jdR4qlmZHhONVM5AQOAWXuF/w3dnHbEQVrNptJgt7F+cQ==}
engines: {node: '>=10.13.0'}
dependencies:
graceful-fs: 4.2.10
graceful-fs: 4.2.11
tapable: 2.2.1
/enhanced-resolve/5.12.0:
@ -6014,7 +6014,7 @@ packages:
engines: {node: '>=10'}
dependencies:
at-least-node: 1.0.0
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jsonfile: 6.1.0
universalify: 2.0.0
@ -6163,7 +6163,6 @@ packages:
/graceful-fs/4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
dev: true
/grapheme-splitter/1.0.4:
resolution: {integrity: sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ==}
@ -6805,7 +6804,7 @@ packages:
ci-info: 3.4.0
deepmerge: 4.2.2
glob: 7.2.3
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jest-circus: 27.5.1
jest-environment-jsdom: 27.5.1
jest-environment-node: 27.5.1
@ -6977,7 +6976,7 @@ packages:
'@jest/types': 27.5.1
'@types/stack-utils': 2.0.1
chalk: 4.1.2
graceful-fs: 4.2.10
graceful-fs: 4.2.11
micromatch: 4.0.5
pretty-format: 27.5.1
slash: 3.0.0
@ -6991,7 +6990,7 @@ packages:
'@jest/types': 28.1.3
'@types/stack-utils': 2.0.1
chalk: 4.1.2
graceful-fs: 4.2.10
graceful-fs: 4.2.11
micromatch: 4.0.5
pretty-format: 28.1.3
slash: 3.0.0
@ -7060,7 +7059,7 @@ packages:
'@types/node': 16.11.59
chalk: 4.1.2
emittery: 0.8.1
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jest-docblock: 27.5.1
jest-environment-jsdom: 27.5.1
jest-environment-node: 27.5.1
@ -7095,7 +7094,7 @@ packages:
collect-v8-coverage: 1.0.1
execa: 5.1.1
glob: 7.2.3
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jest-haste-map: 27.5.1
jest-message-util: 27.5.1
jest-mock: 27.5.1
@ -7113,7 +7112,7 @@ packages:
engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0}
dependencies:
'@types/node': 16.11.59
graceful-fs: 4.2.10
graceful-fs: 4.2.11
/jest-snapshot/27.5.1:
resolution: {integrity: sha512-yYykXI5a0I31xX67mgeLw1DZ0bJB+gpq5IpSuCAoyDi0+BhgU/RIrL+RTzDmkNTchvDFWKP8lp+w/42Z3us5sA==}
@ -7131,7 +7130,7 @@ packages:
babel-preset-current-node-syntax: 1.0.1_@babel+core@7.19.1
chalk: 4.1.2
expect: 27.5.1
graceful-fs: 4.2.10
graceful-fs: 4.2.11
jest-diff: 27.5.1
jest-get-type: 27.5.1
jest-haste-map: 27.5.1
@ -7163,7 +7162,7 @@ packages:
'@types/node': 16.11.59
chalk: 4.1.2
ci-info: 3.4.0
graceful-fs: 4.2.10
graceful-fs: 4.2.11
picomatch: 2.3.1
/jest-validate/27.5.1:
@ -7366,7 +7365,7 @@ packages:
dependencies:
universalify: 2.0.0
optionalDependencies:
graceful-fs: 4.2.10
graceful-fs: 4.2.11
/jsonp/0.2.1:
resolution: {integrity: sha512-pfog5gdDxPdV4eP7Kg87M8/bHgshlZ5pybl+yKxAnCZ5O7lCIn7Ixydj03wOlnDQesky2BPyA91SQ+5Y/mNwzw==}
@ -10760,7 +10759,7 @@ packages:
engines: {node: '>=10.13.0'}
dependencies:
glob-to-regexp: 0.4.1
graceful-fs: 4.2.10
graceful-fs: 4.2.11
/wbuf/1.7.3:
resolution: {integrity: sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==}

View File

@ -600,6 +600,10 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
'upvote',
'reopened',
'closed',
'pin',
'unpin',
'show',
'hide',
];
export const SYSTEM_AVATAR_OPTIONS = [

View File

@ -535,6 +535,11 @@ export interface User {
avatar: string;
}
export interface QuestionOperationReq {
id: string;
operation: 'pin' | 'unpin' | 'hide' | 'show';
}
export interface OauthBindEmailReq {
binding_key: string;
email: string;

View File

@ -51,7 +51,7 @@ const Index = ({
'd-flex align-items-start flex-column flex-md-row',
className,
)}>
<div>
<div className="w-100">
<div
className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg,

View File

@ -37,7 +37,7 @@ const Index = ({ userName, onSendReply, onCancel, mode }) => {
{t('reply_to')} {userName}
</div>
<div className="d-flex mb-1 align-items-start flex-column flex-md-row">
<div>
<div className="w-100">
<div
className={classNames('custom-form-control', {
'is-invalid': validationErrorMsg,

View File

@ -34,7 +34,9 @@ const ActivateScriptNodes = (el, part) => {
}
scriptList?.forEach((so) => {
const script = document.createElement('script');
script.text = so.text;
script.text = `(() => {
${so.text}
})();`;
for (let i = 0; i < so.attributes.length; i += 1) {
const attr = so.attributes[i];
script.setAttribute(attr.name, attr.value);

View File

@ -8,15 +8,24 @@ interface IProps {
name: string;
className?: string;
size?: string;
title?: string;
onClick?: () => void;
}
const Icon: FC<IProps> = ({ type = 'br', name, className, size, onClick }) => {
const Icon: FC<IProps> = ({
type = 'br',
name,
className,
size,
onClick,
title = '',
}) => {
return (
<i
className={classNames(type, `bi-${name}`, className)}
style={{ ...(size && { fontSize: size }) }}
onClick={onClick}
onKeyDown={onClick}
title={title}
/>
);
};

View File

@ -1,19 +1,22 @@
import { memo, FC } from 'react';
import { Button } from 'react-bootstrap';
import { Button, Dropdown } from 'react-bootstrap';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Modal } from '@/components';
import { useReportModal, useToast } from '@/hooks';
import { QuestionOperationReq } from '@/common/interface';
import Share from '../Share';
import {
deleteQuestion,
deleteAnswer,
editCheck,
reopenQuestion,
questionOpetation,
} from '@/services';
import { tryNormalLogged } from '@/utils/guard';
import { floppyNavigation } from '@/utils';
import { toastStore } from '@/stores';
interface IProps {
type: 'answer' | 'question';
@ -78,7 +81,7 @@ const Index: FC<IProps> = ({
id: qid,
}).then(() => {
toast.onShow({
msg: t('tip_question_deleted'),
msg: t('post_deleted', { keyPrefix: 'messages' }),
variant: 'success',
});
callback?.('delete_question');
@ -134,7 +137,7 @@ const Index: FC<IProps> = ({
question_id: qid,
}).then(() => {
toast.onShow({
msg: t('success', { keyPrefix: 'question_detail.reopen' }),
msg: t('post_reopen', { keyPrefix: 'messages' }),
variant: 'success',
});
refreshQuestion();
@ -143,6 +146,51 @@ const Index: FC<IProps> = ({
});
};
const handleCommon = async (params) => {
await questionOpetation(params);
let msg = '';
if (params.operation === 'pin') {
msg = t('post_pin', { keyPrefix: 'messages' });
}
if (params.operation === 'unpin') {
msg = t('post_unpin', { keyPrefix: 'messages' });
}
if (params.operation === 'hide') {
msg = t('post_hide_list', { keyPrefix: 'messages' });
}
if (params.operation === 'show') {
msg = t('post_show_list', { keyPrefix: 'messages' });
}
toastStore.getState().show({
msg,
variant: 'success',
});
setTimeout(() => {
refreshQuestion();
}, 100);
};
const handlOtherActions = (action) => {
const params: QuestionOperationReq = {
id: qid,
operation: action,
};
if (action === 'pin') {
Modal.confirm({
title: t('title', { keyPrefix: 'question_detail.pin' }),
content: t('content', { keyPrefix: 'question_detail.pin' }),
cancelBtnVariant: 'link',
confirmText: t('confirm_btn', { keyPrefix: 'question_detail.pin' }),
onConfirm: () => {
handleCommon(params);
},
});
} else {
handleCommon(params);
}
};
const handleAction = (action) => {
if (!tryNormalLogged(true)) {
return;
@ -162,8 +210,33 @@ const Index: FC<IProps> = ({
if (action === 'reopen') {
handleReopen();
}
if (
action === 'pin' ||
action === 'unpin' ||
action === 'hide' ||
action === 'show'
) {
handlOtherActions(action);
}
};
const firstAction =
memberActions?.filter(
(v) =>
v.action === 'report' || v.action === 'edit' || v.action === 'delete',
) || [];
const secondAction =
memberActions?.filter(
(v) =>
v.action === 'close' ||
v.action === 'reopen' ||
v.action === 'pin' ||
v.action === 'unpin' ||
v.action === 'hide' ||
v.action === 'show',
) || [];
return (
<div className="d-flex align-items-center">
<Share
@ -173,13 +246,13 @@ const Index: FC<IProps> = ({
title={title}
slugTitle={slugTitle}
/>
{memberActions?.map((item) => {
{firstAction?.map((item) => {
if (item.action === 'edit') {
return (
<Link
key={item.action}
to={editUrl}
className="link-secondary p-0 fs-14 me-3"
className="link-secondary p-0 fs-14 ms-3"
onClick={(evt) => handleEdit(evt, editUrl)}
style={{ lineHeight: '23px' }}>
{item.name}
@ -190,12 +263,32 @@ const Index: FC<IProps> = ({
<Button
key={item.action}
variant="link"
className="link-secondary p-0 fs-14 me-3"
className="link-secondary p-0 fs-14 ms-3"
onClick={() => handleAction(item.action)}>
{item.name}
</Button>
);
})}
{secondAction.length > 0 && (
<Dropdown className="ms-3">
<Dropdown.Toggle
variant="link"
className="link-secondary p-0 fs-14 no-toggle">
{t('action', { keyPrefix: 'question_detail' })}
</Dropdown.Toggle>
<Dropdown.Menu>
{secondAction.map((item) => {
return (
<Dropdown.Item
key={item.action}
onClick={() => handleAction(item.action)}>
{item.name}
</Dropdown.Item>
);
})}
</Dropdown.Menu>
</Dropdown>
)}
</div>
);
};

View File

@ -14,6 +14,7 @@ import {
QueryGroup,
QuestionListLoader,
Counts,
Icon,
} from '@/components';
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
@ -62,6 +63,13 @@ const QuestionList: FC<Props> = ({ source, data, isLoading = false }) => {
key={li.id}
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
<h5 className="text-wrap text-break">
{li.pin === 2 && (
<Icon
name="pin-fill"
className="me-1"
title={t('pinned', { keyPrefix: 'btns' })}
/>
)}
<NavLink
to={pathFactory.questionLanding(li.id, li.url_title)}
className="link-dark">

View File

@ -71,7 +71,7 @@ const Index: FC<IProps> = ({ type, qid, aid, title, slugTitle = '' }) => {
<Dropdown.Toggle
id="dropdown-share"
as="a"
className="no-toggle fs-14 link-secondary pointer me-3"
className="no-toggle fs-14 link-secondary pointer"
onClick={() => setShow(true)}
style={{ lineHeight: '23px' }}>
{t('share.name')}

View File

@ -38,7 +38,7 @@ const TagSelector: FC<IProps> = ({
const [initialValue, setInitialValue] = useState<Type.Tag[]>([...value]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [repeatIndex, setRepeatIndex] = useState(-1);
const [tag, setTag] = useState<string>('');
const [searchValue, setSearchValue] = useState<string>('');
const [tags, setTags] = useState<Type.Tag[] | null>(null);
const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' });
const [visibleMenu, setVisibleMenu] = useState(false);
@ -101,12 +101,12 @@ const TagSelector: FC<IProps> = ({
const fetchTags = (str) => {
queryTags(str).then((res) => {
const tagArray: Type.Tag[] = filterTags(res || []);
setTags(tagArray);
setTags(tagArray?.length > 5 ? tagArray.slice(0, 5) : tagArray);
});
};
useEffect(() => {
fetchTags(tag);
fetchTags(searchValue);
}, [visibleMenu]);
const handleClick = (val: Type.Tag) => {
@ -146,7 +146,7 @@ const TagSelector: FC<IProps> = ({
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
const searchStr = e.currentTarget.value.replace(';', '');
setTag(searchStr);
setSearchValue(searchStr);
fetchTags(searchStr);
};
@ -172,7 +172,7 @@ const TagSelector: FC<IProps> = ({
e.preventDefault();
if (tags.length === 0) {
tagModal.onShow(tag);
tagModal.onShow(searchValue);
return;
}
if (currentIndex <= tags.length - 1) {
@ -228,13 +228,14 @@ const TagSelector: FC<IProps> = ({
<FormControl
placeholder={t('search_tag')}
autoFocus
value={tag}
value={searchValue}
onChange={handleSearch}
/>
</Form>
</Dropdown.Header>
)}
{showRequiredTagText &&
{!searchValue &&
showRequiredTagText &&
tags &&
tags.filter((v) => v.recommend)?.length > 0 && (
<h6 className="dropdown-header">{t('tag_required_text')}</h6>
@ -251,17 +252,17 @@ const TagSelector: FC<IProps> = ({
</Dropdown.Item>
);
})}
{tag && tags && tags.length === 0 && (
{searchValue && tags && tags.length === 0 && (
<Dropdown.Item disabled className="text-secondary">
{t('no_result')}
</Dropdown.Item>
)}
{!hiddenCreateBtn && tag && (
{!hiddenCreateBtn && searchValue && (
<Button
variant="link"
className="px-3 btn-no-border w-100 text-start"
onClick={() => {
tagModal.onShow(tag);
tagModal.onShow(searchValue);
}}>
+ {t('create_btn')}
</Button>

View File

@ -11,6 +11,7 @@ import {
Comment,
FormatTime,
htmlRender,
Icon,
} from '@/components';
import { formatCount, guard } from '@/utils';
import { following } from '@/services';
@ -65,6 +66,13 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
return (
<div>
<h1 className="h3 mb-3 text-wrap text-break">
{data?.pin === 2 && (
<Icon
name="pin-fill"
className="me-1"
title={t('pinned', { keyPrefix: 'btns' })}
/>
)}
<Link
className="link-dark"
reloadDocument

View File

@ -300,7 +300,7 @@ const Index: React.FC = () => {
</Form.Control.Feedback>
</Form.Group>
<Form.Group className="mb-3">
<Form.Group controlId="avatar" className="mb-3">
<Form.Label>{t('avatar.label')}</Form.Label>
<div className="mb-3">
<Form.Select

View File

@ -278,3 +278,7 @@ export const markdownToHtml = (content: string) => {
export const saveQuestionWidthAnaser = (params: Type.QuestionWithAnswer) => {
return request.post('/answer/api/v1/question/answer', params);
};
export const questionOpetation = (params: Type.QuestionOperationReq) => {
return request.put('/answer/api/v1/question/operation', params);
};