mirror of https://gitee.com/answerdev/answer.git
Merge remote-tracking branch 'github/feat/1.1.2/ui' into feat/1.1.2/user-center
# Conflicts: # Makefile # i18n/en_US.yaml # internal/migrations/migrations.go
This commit is contained in:
commit
8fb303d024
62
docs/docs.go
62
docs/docs.go
|
@ -3654,6 +3654,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": {
|
"/answer/api/v1/question/page": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "get questions by page",
|
"description": "get questions by page",
|
||||||
|
@ -7442,6 +7481,21 @@ const docTemplate = `{
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"schema.OperationQuestionReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"description": "operation [pin unpin hide show]",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"schema.PermissionMemberAction": {
|
"schema.PermissionMemberAction": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -7611,6 +7665,14 @@ const docTemplate = `{
|
||||||
"operator": {
|
"operator": {
|
||||||
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
||||||
},
|
},
|
||||||
|
"pin": {
|
||||||
|
"description": "1: unpin, 2: pin",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"description": "0: show, 1: hide",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3642,6 +3642,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": {
|
"/answer/api/v1/question/page": {
|
||||||
"get": {
|
"get": {
|
||||||
"description": "get questions by page",
|
"description": "get questions by page",
|
||||||
|
@ -7430,6 +7469,21 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"schema.OperationQuestionReq": {
|
||||||
|
"type": "object",
|
||||||
|
"required": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"properties": {
|
||||||
|
"id": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"operation": {
|
||||||
|
"description": "operation [pin unpin hide show]",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"schema.PermissionMemberAction": {
|
"schema.PermissionMemberAction": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
@ -7599,6 +7653,14 @@
|
||||||
"operator": {
|
"operator": {
|
||||||
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
"$ref": "#/definitions/schema.QuestionPageRespOperator"
|
||||||
},
|
},
|
||||||
|
"pin": {
|
||||||
|
"description": "1: unpin, 2: pin",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"description": "0: show, 1: hide",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
"status": {
|
"status": {
|
||||||
"type": "integer"
|
"type": "integer"
|
||||||
},
|
},
|
||||||
|
|
|
@ -1128,6 +1128,16 @@ definitions:
|
||||||
description: inbox achievement
|
description: inbox achievement
|
||||||
type: string
|
type: string
|
||||||
type: object
|
type: object
|
||||||
|
schema.OperationQuestionReq:
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: string
|
||||||
|
operation:
|
||||||
|
description: operation [pin unpin hide show]
|
||||||
|
type: string
|
||||||
|
required:
|
||||||
|
- id
|
||||||
|
type: object
|
||||||
schema.PermissionMemberAction:
|
schema.PermissionMemberAction:
|
||||||
properties:
|
properties:
|
||||||
action:
|
action:
|
||||||
|
@ -1249,6 +1259,12 @@ definitions:
|
||||||
type: string
|
type: string
|
||||||
operator:
|
operator:
|
||||||
$ref: '#/definitions/schema.QuestionPageRespOperator'
|
$ref: '#/definitions/schema.QuestionPageRespOperator'
|
||||||
|
pin:
|
||||||
|
description: '1: unpin, 2: pin'
|
||||||
|
type: integer
|
||||||
|
show:
|
||||||
|
description: '0: show, 1: hide'
|
||||||
|
type: integer
|
||||||
status:
|
status:
|
||||||
type: integer
|
type: integer
|
||||||
tags:
|
tags:
|
||||||
|
@ -4405,6 +4421,30 @@ paths:
|
||||||
summary: get question details
|
summary: get question details
|
||||||
tags:
|
tags:
|
||||||
- Question
|
- 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:
|
/answer/api/v1/question/page:
|
||||||
get:
|
get:
|
||||||
consumes:
|
consumes:
|
||||||
|
|
|
@ -25,6 +25,14 @@ backend:
|
||||||
other: Reopen
|
other: Reopen
|
||||||
forbidden_error:
|
forbidden_error:
|
||||||
other: Forbidden.
|
other: Forbidden.
|
||||||
|
pin:
|
||||||
|
other: Pin
|
||||||
|
hide:
|
||||||
|
other: Unlist
|
||||||
|
unpin:
|
||||||
|
other: Unpin
|
||||||
|
show:
|
||||||
|
other: List
|
||||||
role:
|
role:
|
||||||
name:
|
name:
|
||||||
user:
|
user:
|
||||||
|
@ -868,6 +876,7 @@ ui:
|
||||||
btn: Add question
|
btn: Add question
|
||||||
answers: answers
|
answers: answers
|
||||||
question_detail:
|
question_detail:
|
||||||
|
action: Action
|
||||||
Asked: Asked
|
Asked: Asked
|
||||||
asked: asked
|
asked: asked
|
||||||
update: Modified
|
update: Modified
|
||||||
|
@ -906,13 +915,14 @@ ui:
|
||||||
li1_2: Back up any statements you make with references or personal experience.
|
li1_2: Back up any statements you make with references or personal experience.
|
||||||
header_2: But <strong>avoid</strong> ...
|
header_2: But <strong>avoid</strong> ...
|
||||||
li2_1: Asking for help, seeking clarification, or responding to other answers.
|
li2_1: Asking for help, seeking clarification, or responding to other answers.
|
||||||
|
|
||||||
reopen:
|
reopen:
|
||||||
confirm_btn: Reopen
|
confirm_btn: Reopen
|
||||||
title: Reopen this post
|
title: Reopen this post
|
||||||
content: Are you sure you want to reopen?
|
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:
|
delete:
|
||||||
title: Delete this post
|
title: Delete this post
|
||||||
question: >-
|
question: >-
|
||||||
|
@ -926,7 +936,6 @@ ui:
|
||||||
of accepted answers can result in your account being blocked from answering.
|
of accepted answers can result in your account being blocked from answering.
|
||||||
Are you sure you wish to delete?
|
Are you sure you wish to delete?
|
||||||
other: 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
|
tip_answer_deleted: This answer has been deleted
|
||||||
btns:
|
btns:
|
||||||
confirm: Confirm
|
confirm: Confirm
|
||||||
|
@ -942,6 +951,7 @@ ui:
|
||||||
reject: Reject
|
reject: Reject
|
||||||
skip: Skip
|
skip: Skip
|
||||||
discard_draft: Discard draft
|
discard_draft: Discard draft
|
||||||
|
pinned: Pinned
|
||||||
search:
|
search:
|
||||||
title: Search Results
|
title: Search Results
|
||||||
keywords: Keywords
|
keywords: Keywords
|
||||||
|
@ -1574,6 +1584,10 @@ ui:
|
||||||
closed: closed
|
closed: closed
|
||||||
reopened: reopened
|
reopened: reopened
|
||||||
created: created
|
created: created
|
||||||
|
pin: pinned
|
||||||
|
unpin: unpinned
|
||||||
|
show: listed
|
||||||
|
hide: unlisted
|
||||||
title: "History for"
|
title: "History for"
|
||||||
tag_title: "Timeline for"
|
tag_title: "Timeline for"
|
||||||
show_votes: "Show votes"
|
show_votes: "Show votes"
|
||||||
|
@ -1599,5 +1613,9 @@ ui:
|
||||||
draft:
|
draft:
|
||||||
discard_confirm: Are you sure you want to discard your draft?
|
discard_confirm: Are you sure you want to discard your draft?
|
||||||
messages:
|
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: 关闭
|
other: 关闭
|
||||||
reopen:
|
reopen:
|
||||||
other: 重新打开
|
other: 重新打开
|
||||||
|
pin:
|
||||||
|
other: 置顶
|
||||||
|
hide:
|
||||||
|
other: 隐藏
|
||||||
|
unpin:
|
||||||
|
other: 取消置顶
|
||||||
|
show:
|
||||||
|
other: 显示
|
||||||
role:
|
role:
|
||||||
name:
|
name:
|
||||||
user:
|
user:
|
||||||
|
|
|
@ -14,6 +14,10 @@ const (
|
||||||
ActFollow = "follow"
|
ActFollow = "follow"
|
||||||
ActAccepted = "accepted"
|
ActAccepted = "accepted"
|
||||||
ActAccept = "accept"
|
ActAccept = "accept"
|
||||||
|
ActPin = "pin"
|
||||||
|
ActUnPin = "unpin"
|
||||||
|
ActShow = "show"
|
||||||
|
ActHide = "hide"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -29,6 +33,10 @@ const (
|
||||||
ActQuestionRollback ActivityTypeKey = "question.rollback"
|
ActQuestionRollback ActivityTypeKey = "question.rollback"
|
||||||
ActQuestionDeleted ActivityTypeKey = "question.deleted"
|
ActQuestionDeleted ActivityTypeKey = "question.deleted"
|
||||||
ActQuestionUndeleted ActivityTypeKey = "question.undeleted"
|
ActQuestionUndeleted ActivityTypeKey = "question.undeleted"
|
||||||
|
ActQuestionPin ActivityTypeKey = "question.pin"
|
||||||
|
ActQuestionUnPin ActivityTypeKey = "question.unpin"
|
||||||
|
ActQuestionHide ActivityTypeKey = "question.hide"
|
||||||
|
ActQuestionShow ActivityTypeKey = "question.show"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
|
|
@ -70,6 +70,47 @@ func (qc *QuestionController) RemoveQuestion(ctx *gin.Context) {
|
||||||
handler.HandleResponse(ctx, err, nil)
|
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
|
// CloseQuestion Close question
|
||||||
// @Summary Close question
|
// @Summary Close question
|
||||||
// @Description Close question
|
// @Description Close question
|
||||||
|
@ -152,6 +193,10 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
||||||
permission.QuestionDelete,
|
permission.QuestionDelete,
|
||||||
permission.QuestionClose,
|
permission.QuestionClose,
|
||||||
permission.QuestionReopen,
|
permission.QuestionReopen,
|
||||||
|
permission.QuestionPin,
|
||||||
|
permission.QuestionUnPin,
|
||||||
|
permission.QuestionHide,
|
||||||
|
permission.QuestionShow,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
handler.HandleResponse(ctx, err, nil)
|
handler.HandleResponse(ctx, err, nil)
|
||||||
|
@ -163,6 +208,10 @@ func (qc *QuestionController) GetQuestion(ctx *gin.Context) {
|
||||||
req.CanDelete = canList[1]
|
req.CanDelete = canList[1]
|
||||||
req.CanClose = canList[2]
|
req.CanClose = canList[2]
|
||||||
req.CanReopen = canList[3]
|
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)
|
info, err := qc.questionService.GetQuestionAndAddPV(ctx, id, userID, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -8,6 +8,10 @@ const (
|
||||||
QuestionStatusAvailable = 1
|
QuestionStatusAvailable = 1
|
||||||
QuestionStatusClosed = 2
|
QuestionStatusClosed = 2
|
||||||
QuestionStatusDeleted = 10
|
QuestionStatusDeleted = 10
|
||||||
|
QuestionUnPin = 1
|
||||||
|
QuestionPin = 2
|
||||||
|
QuestionShow = 1
|
||||||
|
QuestionHide = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
var AdminQuestionSearchStatus = map[string]int{
|
var AdminQuestionSearchStatus = map[string]int{
|
||||||
|
@ -32,6 +36,8 @@ type Question struct {
|
||||||
Title string `xorm:"not null default '' VARCHAR(150) title"`
|
Title string `xorm:"not null default '' VARCHAR(150) title"`
|
||||||
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
|
OriginalText string `xorm:"not null MEDIUMTEXT original_text"`
|
||||||
ParsedText string `xorm:"not null MEDIUMTEXT parsed_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"`
|
Status int `xorm:"not null default 1 INT(11) status"`
|
||||||
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
|
ViewCount int `xorm:"not null default 0 INT(11) view_count"`
|
||||||
UniqueViewCount int `xorm:"not null default 0 INT(11) unique_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 user role", addRoleFeatures, false),
|
||||||
NewMigration("add theme and private mode", addThemeAndPrivateMode, true),
|
NewMigration("add theme and private mode", addThemeAndPrivateMode, true),
|
||||||
NewMigration("add new answer notification", addNewAnswerNotification, true),
|
NewMigration("add new answer notification", addNewAnswerNotification, true),
|
||||||
|
NewMigration("add user pin hide features", addRolePinAndHideFeatures, true),
|
||||||
NewMigration("add plugin", addPlugin, false),
|
NewMigration("add plugin", addPlugin, false),
|
||||||
NewMigration("add login limitations", addLoginLimitations, true),
|
NewMigration("add login limitations", addLoginLimitations, true),
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
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) {
|
func (qr *questionRepo) UpdateAccepted(ctx context.Context, question *entity.Question) (err error) {
|
||||||
question.ID = uid.DeShortID(question.ID)
|
question.ID = uid.DeShortID(question.ID)
|
||||||
_, err = qr.data.DB.Where("id =?", question.ID).Cols("accepted_answer_id").Update(question)
|
_, 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
|
offset := page * pageSize
|
||||||
session := qr.data.DB.Table("question")
|
session := qr.data.DB.Table("question")
|
||||||
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
|
session = session.In("question.status", []int{entity.QuestionStatusAvailable, entity.QuestionStatusClosed})
|
||||||
|
session.And("question.show = ?", entity.QuestionShow)
|
||||||
session = session.Limit(pageSize, offset)
|
session = session.Limit(pageSize, offset)
|
||||||
session = session.OrderBy("question.created_at asc")
|
session = session.OrderBy("question.created_at asc")
|
||||||
err = session.Select("id,title,created_at,post_update_time").Find(&rows)
|
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 {
|
if len(userID) > 0 {
|
||||||
session.And("question.user_id = ?", userID)
|
session.And("question.user_id = ?", userID)
|
||||||
|
} else {
|
||||||
|
session.And("question.show = ?", entity.QuestionShow)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch orderCond {
|
switch orderCond {
|
||||||
case "newest":
|
case "newest":
|
||||||
session.OrderBy("question.created_at DESC")
|
session.OrderBy("question.pin desc,question.created_at DESC")
|
||||||
case "active":
|
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":
|
case "frequent":
|
||||||
session.OrderBy("question.view_count DESC")
|
session.OrderBy("question.pin desc,question.view_count DESC")
|
||||||
case "score":
|
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":
|
case "unanswered":
|
||||||
session.Where("question.last_answer_id = 0")
|
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)
|
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`").
|
ub = builder.MySQL().Select(afs...).From("`answer`").
|
||||||
LeftJoin("`question`", "`question`.id = `answer`.question_id")
|
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}).
|
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)
|
argsQ = append(argsQ, entity.QuestionStatusDeleted, entity.QuestionShow)
|
||||||
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
|
argsA = append(argsA, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
|
||||||
|
|
||||||
for i, word := range words {
|
for i, word := range words {
|
||||||
if i == 0 {
|
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 := builder.MySQL().Select(qfs...).From("question")
|
||||||
|
|
||||||
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted})
|
b.Where(builder.Lt{"`question`.`status`": entity.QuestionStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
||||||
args = append(args, entity.QuestionStatusDeleted)
|
args = append(args, entity.QuestionStatusDeleted, entity.QuestionShow)
|
||||||
|
|
||||||
for i, word := range words {
|
for i, word := range words {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
@ -343,8 +345,8 @@ func (sr *searchRepo) SearchAnswers(ctx context.Context, words []string, tagIDs
|
||||||
LeftJoin("`question`", "`question`.id = `answer`.question_id")
|
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.Lt{"`answer`.`status`": entity.AnswerStatusDeleted})
|
And(builder.Lt{"`answer`.`status`": entity.AnswerStatusDeleted}).And(builder.Eq{"`question`.`show`": entity.QuestionShow})
|
||||||
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted)
|
args = append(args, entity.QuestionStatusDeleted, entity.AnswerStatusDeleted, entity.QuestionShow)
|
||||||
|
|
||||||
for i, word := range words {
|
for i, word := range words {
|
||||||
if i == 0 {
|
if i == 0 {
|
||||||
|
|
|
@ -195,6 +195,7 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
|
||||||
r.PUT("/question", a.questionController.UpdateQuestion)
|
r.PUT("/question", a.questionController.UpdateQuestion)
|
||||||
r.DELETE("/question", a.questionController.RemoveQuestion)
|
r.DELETE("/question", a.questionController.RemoveQuestion)
|
||||||
r.PUT("/question/status", a.questionController.CloseQuestion)
|
r.PUT("/question/status", a.questionController.CloseQuestion)
|
||||||
|
r.PUT("/question/operation", a.questionController.OperationQuestion)
|
||||||
r.PUT("/question/reopen", a.questionController.ReopenQuestion)
|
r.PUT("/question/reopen", a.questionController.ReopenQuestion)
|
||||||
r.GET("/question/similar", a.questionController.SearchByTitleLike)
|
r.GET("/question/similar", a.questionController.SearchByTitleLike)
|
||||||
|
|
||||||
|
|
|
@ -8,9 +8,13 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
SitemapMaxSize = 50000
|
SitemapMaxSize = 50000
|
||||||
SitemapCachekey = "answer@sitemap"
|
SitemapCachekey = "answer@sitemap"
|
||||||
SitemapPageCachekey = "answer@sitemap@page%d"
|
SitemapPageCachekey = "answer@sitemap@page%d"
|
||||||
|
QuestionOperationPin = "pin"
|
||||||
|
QuestionOperationUnPin = "unpin"
|
||||||
|
QuestionOperationHide = "hide"
|
||||||
|
QuestionOperationShow = "show"
|
||||||
)
|
)
|
||||||
|
|
||||||
// RemoveQuestionReq delete question request
|
// RemoveQuestionReq delete question request
|
||||||
|
@ -28,6 +32,14 @@ type CloseQuestionReq struct {
|
||||||
UserID string `json:"-"` // user_id
|
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 {
|
type CloseQuestionMeta struct {
|
||||||
CloseType int `json:"close_type"`
|
CloseType int `json:"close_type"`
|
||||||
CloseMsg string `json:"close_msg"`
|
CloseMsg string `json:"close_msg"`
|
||||||
|
@ -101,6 +113,12 @@ type QuestionPermission struct {
|
||||||
CanClose bool `json:"-"`
|
CanClose bool `json:"-"`
|
||||||
// whether user can reopen it
|
// whether user can reopen it
|
||||||
CanReopen bool `json:"-"`
|
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
|
// whether user can use reserved it
|
||||||
CanUseReservedTag bool `json:"-"`
|
CanUseReservedTag bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
@ -168,6 +186,8 @@ type QuestionInfo struct {
|
||||||
UpdateTime int64 `json:"-"` // update_time
|
UpdateTime int64 `json:"-"` // update_time
|
||||||
PostUpdateTime int64 `json:"update_time"`
|
PostUpdateTime int64 `json:"update_time"`
|
||||||
QuestionUpdateTime int64 `json:"edit_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"`
|
Status int `json:"status"`
|
||||||
Operation *Operation `json:"operation,omitempty"`
|
Operation *Operation `json:"operation,omitempty"`
|
||||||
UserID string `json:"-" `
|
UserID string `json:"-" `
|
||||||
|
@ -295,6 +315,8 @@ type QuestionPageResp struct {
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
UrlTitle string `json:"url_title"`
|
UrlTitle string `json:"url_title"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Pin int `json:"pin"` // 1: unpin, 2: pin
|
||||||
|
Show int `json:"show"` // 0: show, 1: hide
|
||||||
Status int `json:"status"`
|
Status int `json:"status"`
|
||||||
Tags []*TagResp `json:"tags"`
|
Tags []*TagResp `json:"tags"`
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,10 @@ const (
|
||||||
QuestionReopen = "question.reopen"
|
QuestionReopen = "question.reopen"
|
||||||
QuestionVoteUp = "question.vote_up"
|
QuestionVoteUp = "question.vote_up"
|
||||||
QuestionVoteDown = "question.vote_down"
|
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"
|
AnswerAdd = "answer.add"
|
||||||
AnswerEdit = "answer.edit"
|
AnswerEdit = "answer.edit"
|
||||||
AnswerEditWithoutReview = "answer.edit_without_review"
|
AnswerEditWithoutReview = "answer.edit_without_review"
|
||||||
|
@ -43,4 +47,8 @@ const (
|
||||||
deleteActionName = "action.delete"
|
deleteActionName = "action.delete"
|
||||||
closeActionName = "action.close"
|
closeActionName = "action.close"
|
||||||
reopenActionName = "action.reopen"
|
reopenActionName = "action.reopen"
|
||||||
|
pinActionName = "action.pin"
|
||||||
|
unpinActionName = "action.unpin"
|
||||||
|
hideActionName = "action.hide"
|
||||||
|
showActionName = "action.show"
|
||||||
)
|
)
|
||||||
|
|
|
@ -10,7 +10,7 @@ import (
|
||||||
|
|
||||||
// GetQuestionPermission get question permission
|
// GetQuestionPermission get question permission
|
||||||
func GetQuestionPermission(ctx context.Context, userID string, creatorUserID string,
|
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) {
|
actions []*schema.PermissionMemberAction) {
|
||||||
lang := handler.GetLangByCtx(ctx)
|
lang := handler.GetLangByCtx(ctx)
|
||||||
actions = make([]*schema.PermissionMemberAction, 0)
|
actions = make([]*schema.PermissionMemberAction, 0)
|
||||||
|
@ -42,6 +42,36 @@ func GetQuestionPermission(ctx context.Context, userID string, creatorUserID str
|
||||||
Type: "confirm",
|
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 {
|
if canDelete || userID == creatorUserID {
|
||||||
actions = append(actions, &schema.PermissionMemberAction{
|
actions = append(actions, &schema.PermissionMemberAction{
|
||||||
Action: "delete",
|
Action: "delete",
|
||||||
|
|
|
@ -36,6 +36,7 @@ type QuestionRepo interface {
|
||||||
questionList []*entity.Question, total int64, err error)
|
questionList []*entity.Question, total int64, err error)
|
||||||
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
|
UpdateQuestionStatus(ctx context.Context, question *entity.Question) (err error)
|
||||||
UpdateQuestionStatusWithOutUpdateTime(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)
|
SearchByTitleLike(ctx context.Context, title string) (questionList []*entity.Question, err error)
|
||||||
UpdatePvCount(ctx context.Context, questionID string) (err error)
|
UpdatePvCount(ctx context.Context, questionID string) (err error)
|
||||||
UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error)
|
UpdateAnswerCount(ctx context.Context, questionID string, num int) (err error)
|
||||||
|
@ -271,6 +272,8 @@ func (qs *QuestionCommon) FormatQuestionsPage(
|
||||||
FollowCount: questionInfo.FollowCount,
|
FollowCount: questionInfo.FollowCount,
|
||||||
AcceptedAnswerID: questionInfo.AcceptedAnswerID,
|
AcceptedAnswerID: questionInfo.AcceptedAnswerID,
|
||||||
LastAnswerID: questionInfo.LastAnswerID,
|
LastAnswerID: questionInfo.LastAnswerID,
|
||||||
|
Pin: questionInfo.Pin,
|
||||||
|
Show: questionInfo.Show,
|
||||||
}
|
}
|
||||||
|
|
||||||
questionIDs = append(questionIDs, questionInfo.ID)
|
questionIDs = append(questionIDs, questionInfo.ID)
|
||||||
|
@ -526,6 +529,8 @@ func (qs *QuestionCommon) ShowFormat(ctx context.Context, data *entity.Question)
|
||||||
info.QuestionUpdateTime = 0
|
info.QuestionUpdateTime = 0
|
||||||
}
|
}
|
||||||
info.Status = data.Status
|
info.Status = data.Status
|
||||||
|
info.Pin = data.Pin
|
||||||
|
info.Show = data.Show
|
||||||
info.UserID = data.UserID
|
info.UserID = data.UserID
|
||||||
info.LastEditUserID = data.LastEditUserID
|
info.LastEditUserID = data.LastEditUserID
|
||||||
if data.LastAnswerID != "0" {
|
if data.LastAnswerID != "0" {
|
||||||
|
|
|
@ -270,6 +270,8 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
||||||
question.Status = entity.QuestionStatusAvailable
|
question.Status = entity.QuestionStatusAvailable
|
||||||
question.RevisionID = "0"
|
question.RevisionID = "0"
|
||||||
question.CreatedAt = now
|
question.CreatedAt = now
|
||||||
|
question.Pin = entity.QuestionUnPin
|
||||||
|
question.Show = entity.QuestionShow
|
||||||
//question.UpdatedAt = nil
|
//question.UpdatedAt = nil
|
||||||
err = qs.questionRepo.AddQuestion(ctx, question)
|
err = qs.questionRepo.AddQuestion(ctx, question)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -319,6 +321,58 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
||||||
return
|
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
|
// RemoveQuestion delete question
|
||||||
func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {
|
func (qs *QuestionService) RemoveQuestion(ctx context.Context, req *schema.RemoveQuestionReq) (err error) {
|
||||||
questionInfo, has, err := qs.questionRepo.GetQuestion(ctx, req.ID)
|
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 {
|
if question.Status == entity.QuestionStatusClosed {
|
||||||
per.CanClose = false
|
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 {
|
if question.Status == entity.QuestionStatusDeleted {
|
||||||
operation := &schema.Operation{}
|
operation := &schema.Operation{}
|
||||||
operation.Msg = translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionAlreadyDeleted)
|
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.Description = htmltext.FetchExcerpt(question.HTML, "...", 240)
|
||||||
question.MemberActions = permission.GetQuestionPermission(ctx, userID, question.UserID,
|
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
|
return question, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -735,14 +804,15 @@ func (qs *QuestionService) SearchUserAnswerList(ctx context.Context, userName, o
|
||||||
if ok {
|
if ok {
|
||||||
item.QuestionInfo = questionMaps[item.QuestionID]
|
item.QuestionInfo = questionMaps[item.QuestionID]
|
||||||
}
|
}
|
||||||
}
|
|
||||||
for _, item := range answerlist {
|
|
||||||
info := &schema.UserAnswerInfo{}
|
info := &schema.UserAnswerInfo{}
|
||||||
_ = copier.Copy(info, item)
|
_ = copier.Copy(info, item)
|
||||||
info.AnswerID = item.ID
|
info.AnswerID = item.ID
|
||||||
info.QuestionID = item.QuestionID
|
info.QuestionID = item.QuestionID
|
||||||
userAnswerlist = append(userAnswerlist, info)
|
if item.QuestionInfo.Status != entity.QuestionStatusDeleted {
|
||||||
|
userAnswerlist = append(userAnswerlist, info)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return userAnswerlist, count, nil
|
return userAnswerlist, count, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const {
|
const {
|
||||||
addWebpackModuleRule,
|
addWebpackModuleRule,
|
||||||
addWebpackAlias
|
addWebpackAlias,
|
||||||
} = require("customize-cra");
|
} = require("customize-cra");
|
||||||
|
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
|
@ -82,7 +82,7 @@
|
||||||
"react-app-rewired": "^2.2.1",
|
"react-app-rewired": "^2.2.1",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"sass": "^1.54.4",
|
"sass": "^1.54.4",
|
||||||
"typescript": "^4.9.5",
|
"typescript": "^4.8.3",
|
||||||
"yaml-loader": "^0.8.0"
|
"yaml-loader": "^0.8.0"
|
||||||
},
|
},
|
||||||
"packageManager": "pnpm@7.9.5",
|
"packageManager": "pnpm@7.9.5",
|
||||||
|
|
|
@ -66,7 +66,7 @@ specifiers:
|
||||||
sass: ^1.54.4
|
sass: ^1.54.4
|
||||||
semver: ^7.3.8
|
semver: ^7.3.8
|
||||||
swr: ^1.3.0
|
swr: ^1.3.0
|
||||||
typescript: ^4.9.5
|
typescript: ^4.8.3
|
||||||
urlcat: ^3.0.0
|
urlcat: ^3.0.0
|
||||||
yaml-loader: ^0.8.0
|
yaml-loader: ^0.8.0
|
||||||
zustand: ^4.1.1
|
zustand: ^4.1.1
|
||||||
|
|
|
@ -609,6 +609,10 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
||||||
'upvote',
|
'upvote',
|
||||||
'reopened',
|
'reopened',
|
||||||
'closed',
|
'closed',
|
||||||
|
'pin',
|
||||||
|
'unpin',
|
||||||
|
'show',
|
||||||
|
'hide',
|
||||||
];
|
];
|
||||||
|
|
||||||
export const SYSTEM_AVATAR_OPTIONS = [
|
export const SYSTEM_AVATAR_OPTIONS = [
|
||||||
|
|
|
@ -585,3 +585,8 @@ export interface PluginConfig {
|
||||||
slug_name: string;
|
slug_name: string;
|
||||||
config_fields: PluginItem[];
|
config_fields: PluginItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface QuestionOperationReq {
|
||||||
|
id: string;
|
||||||
|
operation: 'pin' | 'unpin' | 'hide' | 'show';
|
||||||
|
}
|
||||||
|
|
|
@ -34,7 +34,9 @@ const ActivateScriptNodes = (el, part) => {
|
||||||
}
|
}
|
||||||
scriptList?.forEach((so) => {
|
scriptList?.forEach((so) => {
|
||||||
const script = document.createElement('script');
|
const script = document.createElement('script');
|
||||||
script.text = so.text;
|
script.text = `(() => {
|
||||||
|
${so.text}
|
||||||
|
})();`;
|
||||||
for (let i = 0; i < so.attributes.length; i += 1) {
|
for (let i = 0; i < so.attributes.length; i += 1) {
|
||||||
const attr = so.attributes[i];
|
const attr = so.attributes[i];
|
||||||
script.setAttribute(attr.name, attr.value);
|
script.setAttribute(attr.name, attr.value);
|
||||||
|
|
|
@ -8,15 +8,24 @@ interface IProps {
|
||||||
name: string;
|
name: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
size?: string;
|
size?: string;
|
||||||
|
title?: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
}
|
}
|
||||||
const Icon: FC<IProps> = ({ type = 'br', name, className, size, onClick }) => {
|
const Icon: FC<IProps> = ({
|
||||||
|
type = 'br',
|
||||||
|
name,
|
||||||
|
className,
|
||||||
|
size,
|
||||||
|
onClick,
|
||||||
|
title = '',
|
||||||
|
}) => {
|
||||||
return (
|
return (
|
||||||
<i
|
<i
|
||||||
className={classNames(type, `bi-${name}`, className)}
|
className={classNames(type, `bi-${name}`, className)}
|
||||||
style={{ ...(size && { fontSize: size }) }}
|
style={{ ...(size && { fontSize: size }) }}
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
onKeyDown={onClick}
|
onKeyDown={onClick}
|
||||||
|
title={title}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,19 +1,22 @@
|
||||||
import { memo, FC } from 'react';
|
import { memo, FC } from 'react';
|
||||||
import { Button } from 'react-bootstrap';
|
import { Button, Dropdown } from 'react-bootstrap';
|
||||||
import { Link, useNavigate } from 'react-router-dom';
|
import { Link, useNavigate } from 'react-router-dom';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { Modal } from '@/components';
|
import { Modal } from '@/components';
|
||||||
import { useReportModal, useToast } from '@/hooks';
|
import { useReportModal, useToast } from '@/hooks';
|
||||||
|
import { QuestionOperationReq } from '@/common/interface';
|
||||||
import Share from '../Share';
|
import Share from '../Share';
|
||||||
import {
|
import {
|
||||||
deleteQuestion,
|
deleteQuestion,
|
||||||
deleteAnswer,
|
deleteAnswer,
|
||||||
editCheck,
|
editCheck,
|
||||||
reopenQuestion,
|
reopenQuestion,
|
||||||
|
questionOpetation,
|
||||||
} from '@/services';
|
} from '@/services';
|
||||||
import { tryNormalLogged } from '@/utils/guard';
|
import { tryNormalLogged } from '@/utils/guard';
|
||||||
import { floppyNavigation } from '@/utils';
|
import { floppyNavigation } from '@/utils';
|
||||||
|
import { toastStore } from '@/stores';
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
type: 'answer' | 'question';
|
type: 'answer' | 'question';
|
||||||
|
@ -78,7 +81,7 @@ const Index: FC<IProps> = ({
|
||||||
id: qid,
|
id: qid,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
toast.onShow({
|
toast.onShow({
|
||||||
msg: t('tip_question_deleted'),
|
msg: t('post_deleted', { keyPrefix: 'messages' }),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
callback?.('delete_question');
|
callback?.('delete_question');
|
||||||
|
@ -134,7 +137,7 @@ const Index: FC<IProps> = ({
|
||||||
question_id: qid,
|
question_id: qid,
|
||||||
}).then(() => {
|
}).then(() => {
|
||||||
toast.onShow({
|
toast.onShow({
|
||||||
msg: t('success', { keyPrefix: 'question_detail.reopen' }),
|
msg: t('post_reopen', { keyPrefix: 'messages' }),
|
||||||
variant: 'success',
|
variant: 'success',
|
||||||
});
|
});
|
||||||
refreshQuestion();
|
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) => {
|
const handleAction = (action) => {
|
||||||
if (!tryNormalLogged(true)) {
|
if (!tryNormalLogged(true)) {
|
||||||
return;
|
return;
|
||||||
|
@ -162,8 +210,33 @@ const Index: FC<IProps> = ({
|
||||||
if (action === 'reopen') {
|
if (action === 'reopen') {
|
||||||
handleReopen();
|
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 (
|
return (
|
||||||
<div className="d-flex align-items-center">
|
<div className="d-flex align-items-center">
|
||||||
<Share
|
<Share
|
||||||
|
@ -173,13 +246,13 @@ const Index: FC<IProps> = ({
|
||||||
title={title}
|
title={title}
|
||||||
slugTitle={slugTitle}
|
slugTitle={slugTitle}
|
||||||
/>
|
/>
|
||||||
{memberActions?.map((item) => {
|
{firstAction?.map((item) => {
|
||||||
if (item.action === 'edit') {
|
if (item.action === 'edit') {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={item.action}
|
key={item.action}
|
||||||
to={editUrl}
|
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)}
|
onClick={(evt) => handleEdit(evt, editUrl)}
|
||||||
style={{ lineHeight: '23px' }}>
|
style={{ lineHeight: '23px' }}>
|
||||||
{item.name}
|
{item.name}
|
||||||
|
@ -190,12 +263,32 @@ const Index: FC<IProps> = ({
|
||||||
<Button
|
<Button
|
||||||
key={item.action}
|
key={item.action}
|
||||||
variant="link"
|
variant="link"
|
||||||
className="link-secondary p-0 fs-14 me-3"
|
className="link-secondary p-0 fs-14 ms-3"
|
||||||
onClick={() => handleAction(item.action)}>
|
onClick={() => handleAction(item.action)}>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Button>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,6 +14,7 @@ import {
|
||||||
QueryGroup,
|
QueryGroup,
|
||||||
QuestionListLoader,
|
QuestionListLoader,
|
||||||
Counts,
|
Counts,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
|
|
||||||
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
const QuestionOrderKeys: Type.QuestionOrderBy[] = [
|
||||||
|
@ -62,6 +63,13 @@ const QuestionList: FC<Props> = ({ source, data, isLoading = false }) => {
|
||||||
key={li.id}
|
key={li.id}
|
||||||
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
|
className="bg-transparent py-3 px-0 border-start-0 border-end-0">
|
||||||
<h5 className="text-wrap text-break">
|
<h5 className="text-wrap text-break">
|
||||||
|
{li.pin === 2 && (
|
||||||
|
<Icon
|
||||||
|
name="pin-fill"
|
||||||
|
className="me-1"
|
||||||
|
title={t('pinned', { keyPrefix: 'btns' })}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<NavLink
|
<NavLink
|
||||||
to={pathFactory.questionLanding(li.id, li.url_title)}
|
to={pathFactory.questionLanding(li.id, li.url_title)}
|
||||||
className="link-dark">
|
className="link-dark">
|
||||||
|
|
|
@ -71,7 +71,7 @@ const Index: FC<IProps> = ({ type, qid, aid, title, slugTitle = '' }) => {
|
||||||
<Dropdown.Toggle
|
<Dropdown.Toggle
|
||||||
id="dropdown-share"
|
id="dropdown-share"
|
||||||
as="a"
|
as="a"
|
||||||
className="no-toggle fs-14 link-secondary pointer me-3"
|
className="no-toggle fs-14 link-secondary pointer"
|
||||||
onClick={() => setShow(true)}
|
onClick={() => setShow(true)}
|
||||||
style={{ lineHeight: '23px' }}>
|
style={{ lineHeight: '23px' }}>
|
||||||
{t('share.name')}
|
{t('share.name')}
|
||||||
|
|
|
@ -38,7 +38,7 @@ const TagSelector: FC<IProps> = ({
|
||||||
const [initialValue, setInitialValue] = useState<Type.Tag[]>([...value]);
|
const [initialValue, setInitialValue] = useState<Type.Tag[]>([...value]);
|
||||||
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
const [currentIndex, setCurrentIndex] = useState<number>(0);
|
||||||
const [repeatIndex, setRepeatIndex] = useState(-1);
|
const [repeatIndex, setRepeatIndex] = useState(-1);
|
||||||
const [tag, setTag] = useState<string>('');
|
const [searchValue, setSearchValue] = useState<string>('');
|
||||||
const [tags, setTags] = useState<Type.Tag[] | null>(null);
|
const [tags, setTags] = useState<Type.Tag[] | null>(null);
|
||||||
const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' });
|
const { t } = useTranslation('translation', { keyPrefix: 'tag_selector' });
|
||||||
const [visibleMenu, setVisibleMenu] = useState(false);
|
const [visibleMenu, setVisibleMenu] = useState(false);
|
||||||
|
@ -101,12 +101,12 @@ const TagSelector: FC<IProps> = ({
|
||||||
const fetchTags = (str) => {
|
const fetchTags = (str) => {
|
||||||
queryTags(str).then((res) => {
|
queryTags(str).then((res) => {
|
||||||
const tagArray: Type.Tag[] = filterTags(res || []);
|
const tagArray: Type.Tag[] = filterTags(res || []);
|
||||||
setTags(tagArray);
|
setTags(tagArray?.length > 5 ? tagArray.slice(0, 5) : tagArray);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchTags(tag);
|
fetchTags(searchValue);
|
||||||
}, [visibleMenu]);
|
}, [visibleMenu]);
|
||||||
|
|
||||||
const handleClick = (val: Type.Tag) => {
|
const handleClick = (val: Type.Tag) => {
|
||||||
|
@ -146,7 +146,7 @@ const TagSelector: FC<IProps> = ({
|
||||||
|
|
||||||
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
const handleSearch = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const searchStr = e.currentTarget.value.replace(';', '');
|
const searchStr = e.currentTarget.value.replace(';', '');
|
||||||
setTag(searchStr);
|
setSearchValue(searchStr);
|
||||||
fetchTags(searchStr);
|
fetchTags(searchStr);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -172,7 +172,7 @@ const TagSelector: FC<IProps> = ({
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
if (tags.length === 0) {
|
if (tags.length === 0) {
|
||||||
tagModal.onShow(tag);
|
tagModal.onShow(searchValue);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (currentIndex <= tags.length - 1) {
|
if (currentIndex <= tags.length - 1) {
|
||||||
|
@ -228,13 +228,14 @@ const TagSelector: FC<IProps> = ({
|
||||||
<FormControl
|
<FormControl
|
||||||
placeholder={t('search_tag')}
|
placeholder={t('search_tag')}
|
||||||
autoFocus
|
autoFocus
|
||||||
value={tag}
|
value={searchValue}
|
||||||
onChange={handleSearch}
|
onChange={handleSearch}
|
||||||
/>
|
/>
|
||||||
</Form>
|
</Form>
|
||||||
</Dropdown.Header>
|
</Dropdown.Header>
|
||||||
)}
|
)}
|
||||||
{showRequiredTagText &&
|
{!searchValue &&
|
||||||
|
showRequiredTagText &&
|
||||||
tags &&
|
tags &&
|
||||||
tags.filter((v) => v.recommend)?.length > 0 && (
|
tags.filter((v) => v.recommend)?.length > 0 && (
|
||||||
<h6 className="dropdown-header">{t('tag_required_text')}</h6>
|
<h6 className="dropdown-header">{t('tag_required_text')}</h6>
|
||||||
|
@ -251,17 +252,17 @@ const TagSelector: FC<IProps> = ({
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{tag && tags && tags.length === 0 && (
|
{searchValue && tags && tags.length === 0 && (
|
||||||
<Dropdown.Item disabled className="text-secondary">
|
<Dropdown.Item disabled className="text-secondary">
|
||||||
{t('no_result')}
|
{t('no_result')}
|
||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
)}
|
)}
|
||||||
{!hiddenCreateBtn && tag && (
|
{!hiddenCreateBtn && searchValue && (
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
className="px-3 btn-no-border w-100 text-start"
|
className="px-3 btn-no-border w-100 text-start"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
tagModal.onShow(tag);
|
tagModal.onShow(searchValue);
|
||||||
}}>
|
}}>
|
||||||
+ {t('create_btn')}
|
+ {t('create_btn')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -11,6 +11,7 @@ import {
|
||||||
Comment,
|
Comment,
|
||||||
FormatTime,
|
FormatTime,
|
||||||
htmlRender,
|
htmlRender,
|
||||||
|
Icon,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { formatCount, guard } from '@/utils';
|
import { formatCount, guard } from '@/utils';
|
||||||
import { following } from '@/services';
|
import { following } from '@/services';
|
||||||
|
@ -65,6 +66,13 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<h1 className="h3 mb-3 text-wrap text-break">
|
<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
|
<Link
|
||||||
className="link-dark"
|
className="link-dark"
|
||||||
reloadDocument
|
reloadDocument
|
||||||
|
|
|
@ -324,7 +324,7 @@ const Index: React.FC = () => {
|
||||||
</Form.Control.Feedback>
|
</Form.Control.Feedback>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
<Form.Group className="mb-3">
|
<Form.Group controlId="avatar" className="mb-3">
|
||||||
<Form.Label>{t('avatar.label')}</Form.Label>
|
<Form.Label>{t('avatar.label')}</Form.Label>
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<Form.Select
|
<Form.Select
|
||||||
|
|
|
@ -278,3 +278,7 @@ export const markdownToHtml = (content: string) => {
|
||||||
export const saveQuestionWidthAnaser = (params: Type.QuestionWithAnswer) => {
|
export const saveQuestionWidthAnaser = (params: Type.QuestionWithAnswer) => {
|
||||||
return request.post('/answer/api/v1/question/answer', params);
|
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