mirror of https://gitee.com/answerdev/answer.git
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:
commit
fc9de356d6
62
docs/docs.go
62
docs/docs.go
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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"
|
||||
},
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -22,6 +22,14 @@ backend:
|
|||
other: 关闭
|
||||
reopen:
|
||||
other: 重新打开
|
||||
pin:
|
||||
other: 置顶
|
||||
hide:
|
||||
other: 隐藏
|
||||
unpin:
|
||||
other: 取消置顶
|
||||
show:
|
||||
other: 显示
|
||||
role:
|
||||
name:
|
||||
user:
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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),
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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"`
|
||||
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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" {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
const {
|
||||
addWebpackModuleRule,
|
||||
addWebpackAlias
|
||||
addWebpackAlias,
|
||||
} = require("customize-cra");
|
||||
|
||||
const path = require("path");
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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==}
|
||||
|
|
|
@ -600,6 +600,10 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
|||
'upvote',
|
||||
'reopened',
|
||||
'closed',
|
||||
'pin',
|
||||
'unpin',
|
||||
'show',
|
||||
'hide',
|
||||
];
|
||||
|
||||
export const SYSTEM_AVATAR_OPTIONS = [
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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')}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue