mirror of https://gitee.com/answerdev/answer.git
Merge branch 'test' of git.backyard.segmentfault.com:opensource/answer into test
This commit is contained in:
commit
ec0983395e
|
@ -20,6 +20,5 @@
|
|||
Thumbs*.db
|
||||
tmp
|
||||
vendor/
|
||||
.husky
|
||||
/answer-data/
|
||||
/answer
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
_
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd ui && pnpm commitlint --edit $1 --config commitlint.config.js
|
|
@ -0,0 +1,4 @@
|
|||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
cd ui && npm run pre-commit
|
|
@ -189,7 +189,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
activityCommon := activity_common2.NewActivityCommon(activityRepo)
|
||||
activityActivityRepo := activity.NewActivityRepo(dataData)
|
||||
commentCommonService := comment_common.NewCommentCommonService(commentCommonRepo)
|
||||
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService)
|
||||
activityService := activity2.NewActivityService(activityActivityRepo, userCommon, activityCommon, tagCommonService, objService, commentCommonService, revisionService, metaService)
|
||||
activityController := controller.NewActivityController(activityCommon, activityService)
|
||||
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, controller_backyardReportController, userBackyardController, reasonController, themeController, siteInfoController, siteinfoController, notificationController, dashboardController, uploadController, activityController)
|
||||
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
|
||||
|
|
|
@ -395,6 +395,7 @@ ui:
|
|||
tag_info:
|
||||
created_at: Created
|
||||
edited_at: Edited
|
||||
history: History
|
||||
synonyms:
|
||||
title: Synonyms
|
||||
text: The following tags will be remapped to
|
||||
|
@ -512,6 +513,11 @@ ui:
|
|||
label: Answer
|
||||
msg:
|
||||
empty: Answer cannot be empty.
|
||||
edit_summary:
|
||||
label: Edit Summary
|
||||
placeholder: >-
|
||||
Briefly explain your changes (corrected spelling, fixed grammar,
|
||||
improved formatting)
|
||||
btn_post_question: Post your question
|
||||
btn_save_edits: Save edits
|
||||
answer_question: Answer your own question
|
||||
|
@ -532,6 +538,7 @@ ui:
|
|||
setting: Settings
|
||||
logout: Log out
|
||||
admin: Admin
|
||||
review: Review
|
||||
search:
|
||||
placeholder: Search
|
||||
footer:
|
||||
|
@ -741,6 +748,9 @@ ui:
|
|||
logout: Log out
|
||||
verify: Verify
|
||||
add_question: Add question
|
||||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -1189,3 +1199,36 @@ ui:
|
|||
invalid: is invalid
|
||||
btn_submit: Save
|
||||
not_found_props: "Required property {{ key }} not found."
|
||||
page_review:
|
||||
review: Reivew
|
||||
proposed: proposed
|
||||
question_edit: Question edit
|
||||
answer_edit: Answer edit
|
||||
tag_edit: Tag edit
|
||||
edit_summary: Edit summary
|
||||
empty: No review tasks left.
|
||||
timeline:
|
||||
undeleted: undeleted
|
||||
deleted: deleted
|
||||
downvote: downvote
|
||||
upvote: upvote
|
||||
accept: accept
|
||||
cancelled: cancelled
|
||||
commented: commented
|
||||
rollback: rollback
|
||||
edited: edited
|
||||
answered: answered
|
||||
asked: asked
|
||||
closed: closed
|
||||
reopened: reopened
|
||||
created: created
|
||||
title: "History for"
|
||||
show_votes: "Show votes"
|
||||
n_or_a: N/A
|
||||
title_for: "Timeline for"
|
||||
title_for_tag: "Title for tag"
|
||||
datetime: Datetime
|
||||
type: Type
|
||||
by: By
|
||||
comment: Comment
|
||||
no_data: "We couldn't find anything."
|
||||
|
|
|
@ -1,9 +1,21 @@
|
|||
package constant
|
||||
|
||||
// question activity
|
||||
|
||||
type ActivityTypeKey string
|
||||
|
||||
const (
|
||||
ActEdited = "edited"
|
||||
ActClosed = "closed"
|
||||
ActVotedDown = "voted_down"
|
||||
ActVotedUp = "voted_up"
|
||||
ActVoteDown = "vote_down"
|
||||
ActVoteUp = "vote_up"
|
||||
ActUpVote = "upvote"
|
||||
ActDownVote = "downvote"
|
||||
ActFollow = "follow"
|
||||
ActAccepted = "accepted"
|
||||
ActAccept = "accept"
|
||||
)
|
||||
|
||||
const (
|
||||
ActQuestionAsked ActivityTypeKey = "question.asked"
|
||||
ActQuestionClosed ActivityTypeKey = "question.closed"
|
||||
|
@ -12,29 +24,25 @@ const (
|
|||
ActQuestionCommented ActivityTypeKey = "question.commented"
|
||||
ActQuestionAccept ActivityTypeKey = "question.accept"
|
||||
ActQuestionUpvote ActivityTypeKey = "question.upvote"
|
||||
ActQuestionDownvote ActivityTypeKey = "question.downvote"
|
||||
ActQuestionDownVote ActivityTypeKey = "question.downvote"
|
||||
ActQuestionEdited ActivityTypeKey = "question.edited"
|
||||
ActQuestionRollback ActivityTypeKey = "question.rollback"
|
||||
ActQuestionDeleted ActivityTypeKey = "question.deleted"
|
||||
ActQuestionUndeleted ActivityTypeKey = "question.undeleted"
|
||||
)
|
||||
|
||||
// answer activity
|
||||
|
||||
const (
|
||||
ActAnswerAnswered ActivityTypeKey = "answer.answered"
|
||||
ActAnswerCommented ActivityTypeKey = "answer.commented"
|
||||
ActAnswerAccept ActivityTypeKey = "answer.accept"
|
||||
ActAnswerUpvote ActivityTypeKey = "answer.upvote"
|
||||
ActAnswerDownvote ActivityTypeKey = "answer.downvote"
|
||||
ActAnswerDownVote ActivityTypeKey = "answer.downvote"
|
||||
ActAnswerEdited ActivityTypeKey = "answer.edited"
|
||||
ActAnswerRollback ActivityTypeKey = "answer.rollback"
|
||||
ActAnswerDeleted ActivityTypeKey = "answer.deleted"
|
||||
ActAnswerUndeleted ActivityTypeKey = "answer.undeleted"
|
||||
)
|
||||
|
||||
// tag activity
|
||||
|
||||
const (
|
||||
ActTagCreated ActivityTypeKey = "tag.created"
|
||||
ActTagEdited ActivityTypeKey = "tag.edited"
|
||||
|
|
|
@ -39,6 +39,9 @@ func (ac *ActivityController) GetObjectTimeline(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
if userInfo := middleware.GetUserInfoFromContext(ctx); userInfo != nil {
|
||||
req.IsAdmin = userInfo.IsAdmin
|
||||
}
|
||||
|
||||
resp, err := ac.activityService.GetObjectTimeline(ctx, req)
|
||||
handler.HandleResponse(ctx, err, resp)
|
||||
|
|
|
@ -6,7 +6,6 @@ import (
|
|||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/middleware"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service"
|
||||
"github.com/answerdev/answer/internal/service/dashboard"
|
||||
|
@ -243,10 +242,13 @@ func (ac *AnswerController) Adopted(ctx *gin.Context) {
|
|||
// @Router /answer/admin/api/answer/status [put]
|
||||
// @Success 200 {object} handler.RespBody
|
||||
func (ac *AnswerController) AdminSetAnswerStatus(ctx *gin.Context) {
|
||||
req := &entity.AdminSetAnswerStatusRequest{}
|
||||
req := &schema.AdminSetAnswerStatusRequest{}
|
||||
if handler.BindAndCheck(ctx, req) {
|
||||
return
|
||||
}
|
||||
err := ac.answerService.AdminSetAnswerStatus(ctx, req.AnswerID, req.StatusStr)
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
|
||||
err := ac.answerService.AdminSetAnswerStatus(ctx, req)
|
||||
handler.HandleResponse(ctx, err, gin.H{})
|
||||
}
|
||||
|
|
|
@ -69,7 +69,7 @@ func (tc *TagController) RemoveTag(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
|
||||
err := tc.tagService.RemoveTag(ctx, req.TagID)
|
||||
err := tc.tagService.RemoveTag(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
|
|
|
@ -48,11 +48,6 @@ type CmsAnswerSearch struct {
|
|||
QuestionID string `validate:"omitempty,gt=0,lte=24" json:"question_id" form:"question_id" ` //Query string
|
||||
}
|
||||
|
||||
type AdminSetAnswerStatusRequest struct {
|
||||
StatusStr string `json:"status" form:"status"`
|
||||
AnswerID string `json:"answer_id" form:"answer_id"`
|
||||
}
|
||||
|
||||
// TableName answer table name
|
||||
func (Answer) TableName() string {
|
||||
return "answer"
|
||||
|
|
|
@ -62,11 +62,12 @@ type QuestionWithTagsRevision struct {
|
|||
|
||||
// TagSimpleInfoForRevision tag simple info for revision
|
||||
type TagSimpleInfoForRevision struct {
|
||||
ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
|
||||
MainTagID int64 `xorm:"not null default 0 BIGINT(20) main_tag_id"`
|
||||
SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
|
||||
DisplayName string `xorm:"not null default '' VARCHAR(35) display_name"`
|
||||
Recommend bool `xorm:"not null default false BOOL recommend"`
|
||||
Reserved bool `xorm:"not null default false BOOL reserved"`
|
||||
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
|
||||
ID string `xorm:"not null pk comment('tag_id') BIGINT(20) id"`
|
||||
MainTagID int64 `xorm:"not null default 0 BIGINT(20) main_tag_id"`
|
||||
MainTagSlugName string `xorm:"not null default '' VARCHAR(35) main_tag_slug_name"`
|
||||
SlugName string `xorm:"not null default '' unique VARCHAR(35) slug_name"`
|
||||
DisplayName string `xorm:"not null default '' VARCHAR(35) display_name"`
|
||||
Recommend bool `xorm:"not null default false BOOL recommend"`
|
||||
Reserved bool `xorm:"not null default false BOOL reserved"`
|
||||
RevisionID string `xorm:"not null default 0 BIGINT(20) revision_id"`
|
||||
}
|
||||
|
|
|
@ -3,10 +3,46 @@ package migrations
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addActivityTimeline(x *xorm.Engine) error {
|
||||
defaultConfigTable := []*entity.Config{
|
||||
{ID: 87, Key: "question.asked", Value: `0`},
|
||||
{ID: 88, Key: "question.closed", Value: `0`},
|
||||
{ID: 89, Key: "question.reopened", Value: `0`},
|
||||
{ID: 90, Key: "question.answered", Value: `0`},
|
||||
{ID: 91, Key: "question.commented", Value: `0`},
|
||||
{ID: 92, Key: "question.accept", Value: `0`},
|
||||
{ID: 93, Key: "question.edited", Value: `0`},
|
||||
{ID: 94, Key: "question.rollback", Value: `0`},
|
||||
{ID: 95, Key: "question.deleted", Value: `0`},
|
||||
{ID: 96, Key: "question.undeleted", Value: `0`},
|
||||
{ID: 97, Key: "answer.answered", Value: `0`},
|
||||
{ID: 98, Key: "answer.commented", Value: `0`},
|
||||
{ID: 99, Key: "answer.edited", Value: `0`},
|
||||
{ID: 100, Key: "answer.rollback", Value: `0`},
|
||||
{ID: 101, Key: "answer.undeleted", Value: `0`},
|
||||
{ID: 102, Key: "tag.created", Value: `0`},
|
||||
{ID: 103, Key: "tag.edited", Value: `0`},
|
||||
{ID: 104, Key: "tag.rollback", Value: `0`},
|
||||
{ID: 105, Key: "tag.deleted", Value: `0`},
|
||||
{ID: 106, Key: "tag.undeleted", Value: `0`},
|
||||
}
|
||||
for _, c := range defaultConfigTable {
|
||||
exist, err := x.Get(&entity.Config{ID: c.ID, Key: c.Key})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exist {
|
||||
continue
|
||||
}
|
||||
if _, err := x.Insert(&entity.Config{ID: c.ID, Key: c.Key, Value: c.Value}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
type Revision struct {
|
||||
ReviewUserID int64 `xorm:"not null default 0 BIGINT(20) review_user_id"`
|
||||
}
|
||||
|
|
|
@ -2,10 +2,13 @@ package activity
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/repo/config"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -27,7 +30,23 @@ func NewActivityRepo(
|
|||
func (ar *activityRepo) GetObjectAllActivity(ctx context.Context, objectID string, showVote bool) (
|
||||
activityList []*entity.Activity, err error) {
|
||||
activityList = make([]*entity.Activity, 0)
|
||||
err = ar.data.DB.Find(&activityList, &entity.Activity{OriginalObjectID: objectID})
|
||||
session := ar.data.DB.Desc("created_at")
|
||||
|
||||
if !showVote {
|
||||
var activityTypeNotShown []int
|
||||
for _, obj := range []string{constant.AnswerObjectType, constant.QuestionObjectType, constant.CommentObjectType} {
|
||||
for _, act := range []string{
|
||||
constant.ActVotedDown,
|
||||
constant.ActVotedUp,
|
||||
constant.ActVoteDown,
|
||||
constant.ActVoteUp,
|
||||
} {
|
||||
activityTypeNotShown = append(activityTypeNotShown, config.Key2IDMapping[fmt.Sprintf("%s.%s", obj, act)])
|
||||
}
|
||||
}
|
||||
session.NotIn("activity_type", activityTypeNotShown)
|
||||
}
|
||||
err = session.Find(&activityList, &entity.Activity{OriginalObjectID: objectID})
|
||||
if err != nil {
|
||||
return nil, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
|
|
|
@ -144,9 +144,11 @@ func (ar *AnswerActivityRepo) AcceptAnswer(ctx context.Context,
|
|||
if action == acceptAction {
|
||||
addActivity.UserID = questionUserID
|
||||
addActivity.TriggerUserID = converter.StringToInt64(answerUserID)
|
||||
addActivity.OriginalObjectID = questionObjID // if activity is 'accept' means this question is accept the answer.
|
||||
} else {
|
||||
addActivity.UserID = answerUserID
|
||||
addActivity.TriggerUserID = converter.StringToInt64(answerUserID)
|
||||
addActivity.OriginalObjectID = answerObjID // if activity is 'accepted' means this answer was accepted.
|
||||
}
|
||||
if isSelf {
|
||||
addActivity.Rank = 0
|
||||
|
@ -234,16 +236,17 @@ func (ar *AnswerActivityRepo) CancelAcceptAnswer(ctx context.Context,
|
|||
return errors.InternalServer(reason.DatabaseError).WithError(e).WithStack()
|
||||
}
|
||||
addActivity := &entity.Activity{
|
||||
ObjectID: answerObjID,
|
||||
OriginalObjectID: questionObjID,
|
||||
ActivityType: activityType,
|
||||
Rank: -deltaRank,
|
||||
HasRank: hasRank,
|
||||
ObjectID: answerObjID,
|
||||
ActivityType: activityType,
|
||||
Rank: -deltaRank,
|
||||
HasRank: hasRank,
|
||||
}
|
||||
if action == acceptAction {
|
||||
addActivity.UserID = questionUserID
|
||||
addActivity.OriginalObjectID = questionObjID
|
||||
} else {
|
||||
addActivity.UserID = answerUserID
|
||||
addActivity.OriginalObjectID = answerObjID
|
||||
}
|
||||
addActivityList = append(addActivityList, addActivity)
|
||||
}
|
||||
|
|
|
@ -54,7 +54,7 @@ func Test_tagRepo_GetTagByID(t *testing.T) {
|
|||
tagOnce.Do(addTagList)
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, testTagList[0].SlugName, gotTag.SlugName)
|
||||
|
@ -139,7 +139,7 @@ func Test_tagRepo_UpdateTag(t *testing.T) {
|
|||
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, testTagList[0].DisplayName, gotTag.DisplayName)
|
||||
|
@ -152,7 +152,7 @@ func Test_tagRepo_UpdateTagQuestionCount(t *testing.T) {
|
|||
err := tagCommonRepo.UpdateTagQuestionCount(context.TODO(), testTagList[0].ID, 100)
|
||||
assert.NoError(t, err)
|
||||
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID)
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[0].ID, true)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, 100, gotTag.QuestionCount)
|
||||
|
@ -172,7 +172,7 @@ func Test_tagRepo_UpdateTagSynonym(t *testing.T) {
|
|||
|
||||
tagCommonRepo := tag_common.NewTagCommonRepo(testDataSource, unique.NewUniqueIDRepo(testDataSource))
|
||||
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID)
|
||||
gotTag, exist, err := tagCommonRepo.GetTagByID(context.TODO(), testTagList[2].ID, true)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, exist)
|
||||
assert.Equal(t, testTagList[0].ID, fmt.Sprintf("%d", gotTag.MainTagID))
|
||||
|
|
|
@ -122,12 +122,14 @@ func (tr *tagCommonRepo) GetTagListByNames(ctx context.Context, names []string)
|
|||
}
|
||||
|
||||
// GetTagByID get tag one
|
||||
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string) (
|
||||
func (tr *tagCommonRepo) GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (
|
||||
tag *entity.Tag, exist bool, err error,
|
||||
) {
|
||||
tag = &entity.Tag{}
|
||||
session := tr.data.DB.Where(builder.Eq{"id": tagID})
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
if !includeDeleted {
|
||||
session.Where(builder.Eq{"status": entity.TagStatusAvailable})
|
||||
}
|
||||
exist, err = session.Get(tag)
|
||||
if err != nil {
|
||||
return nil, false, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
|
|
@ -17,6 +17,7 @@ type GetObjectTimelineReq struct {
|
|||
ObjectID string `validate:"omitempty,gt=0,lte=100" form:"object_id"`
|
||||
ShowVote bool `validate:"omitempty" form:"show_vote"`
|
||||
UserID string `json:"-"`
|
||||
IsAdmin bool `json:"-"`
|
||||
}
|
||||
|
||||
// GetObjectTimelineResp get object timeline response
|
||||
|
@ -38,14 +39,16 @@ type ActObjectTimeline struct {
|
|||
ObjectType string `json:"object_type"`
|
||||
Cancelled bool `json:"cancelled"`
|
||||
CancelledAt int64 `json:"cancelled_at"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
// ActObjectInfo act object info
|
||||
type ActObjectInfo struct {
|
||||
Title string `json:"title"`
|
||||
ObjectType string `json:"object_type"`
|
||||
QuestionID string `json:"question_id"`
|
||||
AnswerID string `json:"answer_id"`
|
||||
ObjectType string `json:"object_type"`
|
||||
Title string `json:"title"`
|
||||
QuestionID string `json:"question_id"`
|
||||
AnswerID string `json:"answer_id"`
|
||||
MainTagSlugName string `json:"main_tag_slug_name"`
|
||||
}
|
||||
|
||||
// GetObjectTimelineDetailReq get object timeline detail request
|
||||
|
@ -63,7 +66,18 @@ type GetObjectTimelineDetailResp struct {
|
|||
|
||||
// ObjectTimelineDetail object timeline detail
|
||||
type ObjectTimelineDetail struct {
|
||||
Title string `json:"title"`
|
||||
Tags []string `json:"tags"`
|
||||
OriginalText string `json:"original_text"`
|
||||
Title string `json:"title"`
|
||||
Tags []*ObjectTimelineTag `json:"tags"`
|
||||
OriginalText string `json:"original_text"`
|
||||
SlugName string `json:"slug_name"`
|
||||
MainTagSlugName string `json:"main_tag_slug_name"`
|
||||
}
|
||||
|
||||
// ObjectTimelineTag object timeline tags
|
||||
type ObjectTimelineTag struct {
|
||||
SlugName string `json:"slug_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
MainTagSlugName string `json:"main_tag_slug_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
|
|
@ -77,7 +77,13 @@ type AdminAnswerInfo struct {
|
|||
}
|
||||
|
||||
type AnswerAdoptedReq struct {
|
||||
QuestionID string `json:"question_id" ` // question_id
|
||||
AnswerID string `json:"answer_id" `
|
||||
QuestionID string `json:"question_id"`
|
||||
AnswerID string `json:"answer_id"`
|
||||
UserID string `json:"-" `
|
||||
}
|
||||
|
||||
type AdminSetAnswerStatusRequest struct {
|
||||
StatusStr string `json:"status"`
|
||||
AnswerID string `json:"answer_id"`
|
||||
UserID string `json:"-" `
|
||||
}
|
||||
|
|
|
@ -11,11 +11,13 @@ import (
|
|||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/comment_common"
|
||||
"github.com/answerdev/answer/internal/service/meta"
|
||||
"github.com/answerdev/answer/internal/service/object_info"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
"github.com/answerdev/answer/internal/service/tag_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/answerdev/answer/pkg/obj"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
)
|
||||
|
||||
|
@ -33,6 +35,7 @@ type ActivityService struct {
|
|||
objectInfoService *object_info.ObjService
|
||||
commentCommonService *comment_common.CommentCommonService
|
||||
revisionService *revision_common.RevisionService
|
||||
metaService *meta.MetaService
|
||||
}
|
||||
|
||||
// NewActivityService new activity service
|
||||
|
@ -44,6 +47,7 @@ func NewActivityService(
|
|||
objectInfoService *object_info.ObjService,
|
||||
commentCommonService *comment_common.CommentCommonService,
|
||||
revisionService *revision_common.RevisionService,
|
||||
metaService *meta.MetaService,
|
||||
) *ActivityService {
|
||||
return &ActivityService{
|
||||
objectInfoService: objectInfoService,
|
||||
|
@ -53,6 +57,7 @@ func NewActivityService(
|
|||
tagCommonService: tagCommonService,
|
||||
commentCommonService: commentCommonService,
|
||||
revisionService: revisionService,
|
||||
metaService: metaService,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -64,14 +69,10 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge
|
|||
Timeline: make([]*schema.ActObjectTimeline, 0),
|
||||
}
|
||||
|
||||
objInfo, err := as.objectInfoService.GetInfo(ctx, req.ObjectID)
|
||||
resp.ObjectInfo, err = as.getTimelineMainObjInfo(ctx, req.ObjectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.ObjectInfo.Title = objInfo.Title
|
||||
resp.ObjectInfo.ObjectType = objInfo.ObjectType
|
||||
resp.ObjectInfo.QuestionID = objInfo.QuestionID
|
||||
resp.ObjectInfo.AnswerID = objInfo.AnswerID
|
||||
|
||||
activityList, err := as.activityRepo.GetObjectAllActivity(ctx, req.ObjectID, req.ShowVote)
|
||||
if err != nil {
|
||||
|
@ -85,67 +86,144 @@ func (as *ActivityService) GetObjectTimeline(ctx context.Context, req *schema.Ge
|
|||
Cancelled: act.Cancelled == entity.ActivityCancelled,
|
||||
ObjectID: act.ObjectID,
|
||||
}
|
||||
item.ObjectType, _ = obj.GetObjectTypeStrByObjectID(act.ObjectID)
|
||||
if item.Cancelled {
|
||||
item.CancelledAt = act.CancelledAt.Unix()
|
||||
}
|
||||
|
||||
// database save activity type is number, change to activity type string is like "question.asked".
|
||||
// so we need to cut the front part of '.'
|
||||
item.ObjectType, item.ActivityType, _ = strings.Cut(config.ID2KeyMapping[act.ActivityType], ".")
|
||||
|
||||
isHidden, formattedActivityType := formatActivity(item.ActivityType)
|
||||
if isHidden {
|
||||
_, item.ActivityType, _ = strings.Cut(config.ID2KeyMapping[act.ActivityType], ".")
|
||||
// format activity type string to show
|
||||
if isHidden, formattedActivityType := formatActivity(item.ActivityType); isHidden {
|
||||
continue
|
||||
}
|
||||
item.ActivityType = formattedActivityType
|
||||
|
||||
// get user info
|
||||
userBasicInfo, exist, err := as.userCommon.GetUserBasicInfoByID(ctx, act.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
item.Username = userBasicInfo.Username
|
||||
item.UserDisplayName = userBasicInfo.DisplayName
|
||||
} else {
|
||||
item.ActivityType = formattedActivityType
|
||||
}
|
||||
|
||||
if item.ObjectType == constant.CommentObjectType {
|
||||
comment, err := as.commentCommonService.GetComment(ctx, item.ObjectID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
item.Comment = comment.ParsedText
|
||||
}
|
||||
// if activity is down vote, only admin can see who does it.
|
||||
if item.ActivityType == constant.ActDownVote && !req.IsAdmin {
|
||||
item.Username = "N/A"
|
||||
item.UserDisplayName = "N/A"
|
||||
} else {
|
||||
item.UserID = act.UserID
|
||||
}
|
||||
|
||||
item.Comment = as.getTimelineActivityComment(ctx, item.ObjectID, item.ObjectType, item.ActivityType, item.RevisionID)
|
||||
resp.Timeline = append(resp.Timeline, item)
|
||||
}
|
||||
as.formatTimelineUserInfo(ctx, resp.Timeline)
|
||||
return
|
||||
}
|
||||
|
||||
func (as *ActivityService) getTimelineMainObjInfo(ctx context.Context, objectID string) (
|
||||
resp *schema.ActObjectInfo, err error) {
|
||||
resp = &schema.ActObjectInfo{}
|
||||
objInfo, err := as.objectInfoService.GetInfo(ctx, objectID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.Title = objInfo.Title
|
||||
if objInfo.ObjectType == constant.TagObjectType {
|
||||
tag, exist, _ := as.tagCommonService.GetTagByID(ctx, objInfo.TagID)
|
||||
if exist {
|
||||
resp.Title = tag.SlugName
|
||||
resp.MainTagSlugName = tag.MainTagSlugName
|
||||
}
|
||||
}
|
||||
resp.ObjectType = objInfo.ObjectType
|
||||
resp.QuestionID = objInfo.QuestionID
|
||||
resp.AnswerID = objInfo.AnswerID
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (as *ActivityService) getTimelineActivityComment(ctx context.Context, objectID, objectType,
|
||||
activityType, revisionID string) (comment string) {
|
||||
if objectType == constant.CommentObjectType {
|
||||
commentInfo, err := as.commentCommonService.GetComment(ctx, objectID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
return commentInfo.ParsedText
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if activityType == constant.ActEdited {
|
||||
revision, err := as.revisionService.GetRevision(ctx, revisionID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
return revision.Log
|
||||
}
|
||||
return
|
||||
}
|
||||
if activityType == constant.ActClosed {
|
||||
// only question can be closed
|
||||
metaInfo, err := as.metaService.GetMetaByObjectIdAndKey(ctx, objectID, entity.QuestionCloseReasonKey)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
} else {
|
||||
closeMsg := &schema.CloseQuestionMeta{}
|
||||
if err := json.Unmarshal([]byte(metaInfo.Value), closeMsg); err == nil {
|
||||
return closeMsg.CloseMsg
|
||||
}
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (as *ActivityService) formatTimelineUserInfo(ctx context.Context, timeline []*schema.ActObjectTimeline) {
|
||||
userExist := make(map[string]bool)
|
||||
userIDs := make([]string, 0)
|
||||
for _, info := range timeline {
|
||||
if len(info.UserID) == 0 || userExist[info.UserID] {
|
||||
continue
|
||||
}
|
||||
userIDs = append(userIDs, info.UserID)
|
||||
}
|
||||
if len(userIDs) == 0 {
|
||||
return
|
||||
}
|
||||
userInfoMapping, err := as.userCommon.BatchUserBasicInfoByID(ctx, userIDs)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
for _, info := range timeline {
|
||||
if len(info.UserID) == 0 {
|
||||
continue
|
||||
}
|
||||
if userInfo, ok := userInfoMapping[info.UserID]; ok {
|
||||
info.Username = userInfo.Username
|
||||
info.UserDisplayName = userInfo.DisplayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// GetObjectTimelineDetail get object timeline
|
||||
func (as *ActivityService) GetObjectTimelineDetail(ctx context.Context, req *schema.GetObjectTimelineDetailReq) (
|
||||
resp *schema.GetObjectTimelineDetailResp, err error) {
|
||||
resp = &schema.GetObjectTimelineDetailResp{}
|
||||
resp.OldRevision, err = as.getOneObjectDetail(ctx, req.OldRevisionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.NewRevision, err = as.getOneObjectDetail(ctx, req.NewRevisionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.OldRevision, _ = as.getOneObjectDetail(ctx, req.OldRevisionID)
|
||||
resp.NewRevision, _ = as.getOneObjectDetail(ctx, req.NewRevisionID)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
// GetObjectTimelineDetail get object detail
|
||||
func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID string) (
|
||||
resp *schema.ObjectTimelineDetail, err error) {
|
||||
resp = &schema.ObjectTimelineDetail{Tags: make([]string, 0)}
|
||||
resp = &schema.ObjectTimelineDetail{Tags: make([]*schema.ObjectTimelineTag, 0)}
|
||||
|
||||
// if request revision is 0, return null object detail.
|
||||
if revisionID == "0" {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
revision, err := as.revisionService.GetRevision(ctx, revisionID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
log.Warn(err)
|
||||
return nil, nil
|
||||
}
|
||||
objInfo, err := as.objectInfoService.GetInfo(ctx, revision.ObjectID)
|
||||
if err != nil {
|
||||
|
@ -160,7 +238,13 @@ func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID st
|
|||
return resp, nil
|
||||
}
|
||||
for _, tag := range data.Tags {
|
||||
resp.Tags = append(resp.Tags, tag.SlugName)
|
||||
resp.Tags = append(resp.Tags, &schema.ObjectTimelineTag{
|
||||
SlugName: tag.SlugName,
|
||||
DisplayName: tag.DisplayName,
|
||||
MainTagSlugName: tag.MainTagSlugName,
|
||||
Recommend: tag.Recommend,
|
||||
Reserved: tag.Reserved,
|
||||
})
|
||||
}
|
||||
resp.Title = data.Title
|
||||
resp.OriginalText = data.OriginalText
|
||||
|
@ -178,8 +262,10 @@ func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID st
|
|||
log.Errorf("revision parsing error %s", err)
|
||||
return resp, nil
|
||||
}
|
||||
resp.Title = data.SlugName
|
||||
resp.Title = data.DisplayName
|
||||
resp.OriginalText = data.OriginalText
|
||||
resp.SlugName = data.SlugName
|
||||
resp.MainTagSlugName = data.MainTagSlugName
|
||||
default:
|
||||
log.Errorf("unknown object type %s", objInfo.ObjectType)
|
||||
}
|
||||
|
@ -187,14 +273,19 @@ func (as *ActivityService) getOneObjectDetail(ctx context.Context, revisionID st
|
|||
}
|
||||
|
||||
func formatActivity(activityType string) (isHidden bool, formattedActivityType string) {
|
||||
if activityType == "voted_up" || activityType == "voted_down" || activityType == "accepted" {
|
||||
if activityType == constant.ActVotedUp ||
|
||||
activityType == constant.ActVotedDown ||
|
||||
activityType == constant.ActFollow {
|
||||
return true, ""
|
||||
}
|
||||
if activityType == "vote_up" {
|
||||
return false, "upvote"
|
||||
if activityType == constant.ActVoteUp {
|
||||
return false, constant.ActUpVote
|
||||
}
|
||||
if activityType == "vote_down" {
|
||||
return false, "downvote"
|
||||
if activityType == constant.ActVoteDown {
|
||||
return false, constant.ActDownVote
|
||||
}
|
||||
if activityType == constant.ActAccepted {
|
||||
return false, constant.ActAccept
|
||||
}
|
||||
return false, activityType
|
||||
}
|
||||
|
|
|
@ -7,19 +7,18 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/activity"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
"github.com/answerdev/answer/internal/service/activity_queue"
|
||||
answercommon "github.com/answerdev/answer/internal/service/answer_common"
|
||||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/notice_queue"
|
||||
"github.com/answerdev/answer/internal/service/permission"
|
||||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/revision_common"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -118,6 +117,12 @@ func (as *AnswerService) RemoveAnswer(ctx context.Context, req *schema.RemoveAns
|
|||
if err != nil {
|
||||
log.Errorf("delete answer activity change failed: %s", err.Error())
|
||||
}
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: answerInfo.ID,
|
||||
OriginalObjectID: answerInfo.ID,
|
||||
ActivityTypeKey: constant.ActAnswerDeleted,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -367,12 +372,12 @@ func (as *AnswerService) Get(ctx context.Context, answerID, loginUserID string)
|
|||
return info, questionInfo, has, nil
|
||||
}
|
||||
|
||||
func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID string, setStatusStr string) error {
|
||||
setStatus, ok := entity.CmsAnswerSearchStatus[setStatusStr]
|
||||
func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, req *schema.AdminSetAnswerStatusRequest) error {
|
||||
setStatus, ok := entity.CmsAnswerSearchStatus[req.StatusStr]
|
||||
if !ok {
|
||||
return fmt.Errorf("question status does not exist")
|
||||
}
|
||||
answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, answerID)
|
||||
answerInfo, exist, err := as.answerRepo.GetAnswer(ctx, req.AnswerID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -389,6 +394,13 @@ func (as *AnswerService) AdminSetAnswerStatus(ctx context.Context, answerID stri
|
|||
err = as.answerActivityService.DeleteAnswer(ctx, answerInfo.ID, answerInfo.CreatedAt, answerInfo.VoteCount)
|
||||
if err != nil {
|
||||
log.Errorf("admin delete question then rank rollback error %s", err.Error())
|
||||
} else {
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: answerInfo.ID,
|
||||
OriginalObjectID: answerInfo.ID,
|
||||
ActivityTypeKey: constant.ActAnswerDeleted,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -150,12 +150,19 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
resp.UserStatus = userInfo.Status
|
||||
}
|
||||
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
activityMsg := &schema.ActivityMsg{
|
||||
UserID: comment.UserID,
|
||||
ObjectID: comment.ID,
|
||||
OriginalObjectID: req.ObjectID,
|
||||
ActivityTypeKey: constant.ActQuestionCommented,
|
||||
})
|
||||
}
|
||||
switch objInfo.ObjectType {
|
||||
case constant.QuestionObjectType:
|
||||
activityMsg.ActivityTypeKey = constant.ActQuestionCommented
|
||||
case constant.AnswerObjectType:
|
||||
activityMsg.ActivityTypeKey = constant.ActAnswerCommented
|
||||
}
|
||||
activity_queue.AddActivity(activityMsg)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -95,7 +95,7 @@ func (os *ObjService) GetUnreviewedRevisionInfo(ctx context.Context, objectID st
|
|||
}
|
||||
|
||||
case constant.TagObjectType:
|
||||
tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID)
|
||||
tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -192,7 +192,7 @@ func (os *ObjService) GetInfo(ctx context.Context, objectID string) (objInfo *sc
|
|||
}
|
||||
}
|
||||
case constant.TagObjectType:
|
||||
tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID)
|
||||
tagInfo, exist, err := os.tagRepo.GetTagByID(ctx, objectID, true)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -710,13 +710,19 @@ func (qs *QuestionService) AdminSetQuestionStatus(ctx context.Context, questionI
|
|||
if err != nil {
|
||||
log.Errorf("admin delete question then rank rollback error %s", err.Error())
|
||||
}
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: questionInfo.UserID,
|
||||
ObjectID: questionInfo.ID,
|
||||
OriginalObjectID: questionInfo.ID,
|
||||
ActivityTypeKey: constant.ActQuestionDeleted,
|
||||
})
|
||||
}
|
||||
if setStatus == entity.QuestionStatusAvailable && questionInfo.Status == entity.QuestionStatusClosed {
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: questionInfo.UserID,
|
||||
ObjectID: questionInfo.ID,
|
||||
OriginalObjectID: questionInfo.ID,
|
||||
ActivityTypeKey: constant.ActQuestionDeleted,
|
||||
ActivityTypeKey: constant.ActQuestionReopened,
|
||||
})
|
||||
}
|
||||
if setStatus == entity.QuestionStatusClosed && questionInfo.Status != entity.QuestionStatusClosed {
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/service/revision"
|
||||
usercommon "github.com/answerdev/answer/internal/service/user_common"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
|
@ -47,6 +48,7 @@ func (rs *RevisionService) GetRevision(ctx context.Context, revisionID string) (
|
|||
revision *entity.Revision, err error) {
|
||||
revisionInfo, exist, err := rs.revisionRepo.GetRevisionByID(ctx, revisionID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return nil, err
|
||||
}
|
||||
if !exist {
|
||||
|
|
|
@ -56,13 +56,17 @@ func NewTagService(
|
|||
}
|
||||
|
||||
// RemoveTag delete tag
|
||||
func (ts *TagService) RemoveTag(ctx context.Context, tagID string) (err error) {
|
||||
// TODO permission
|
||||
|
||||
err = ts.tagRepo.RemoveTag(ctx, tagID)
|
||||
func (ts *TagService) RemoveTag(ctx context.Context, req *schema.RemoveTagReq) (err error) {
|
||||
err = ts.tagRepo.RemoveTag(ctx, req.TagID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
activity_queue.AddActivity(&schema.ActivityMsg{
|
||||
UserID: req.UserID,
|
||||
ObjectID: req.TagID,
|
||||
OriginalObjectID: req.TagID,
|
||||
ActivityTypeKey: constant.ActTagDeleted,
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ type TagCommonRepo interface {
|
|||
GetTagBySlugName(ctx context.Context, slugName string) (tagInfo *entity.Tag, exist bool, err error)
|
||||
GetTagListByName(ctx context.Context, name string, limit int, hasReserved bool) (tagList []*entity.Tag, err error)
|
||||
GetTagListByNames(ctx context.Context, names []string) (tagList []*entity.Tag, err error)
|
||||
GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error)
|
||||
GetTagByID(ctx context.Context, tagID string, includeDeleted bool) (tag *entity.Tag, exist bool, err error)
|
||||
GetTagPage(ctx context.Context, page, pageSize int, tag *entity.Tag, queryCond string) (tagList []*entity.Tag, total int64, err error)
|
||||
GetRecommendTagList(ctx context.Context) (tagList []*entity.Tag, err error)
|
||||
GetReservedTagList(ctx context.Context) (tagList []*entity.Tag, err error)
|
||||
|
@ -209,7 +209,7 @@ func (ts *TagCommonService) AddTagList(ctx context.Context, tagList []*entity.Ta
|
|||
|
||||
// GetTagByID get object tag
|
||||
func (ts *TagCommonService) GetTagByID(ctx context.Context, tagID string) (tag *entity.Tag, exist bool, err error) {
|
||||
tag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID)
|
||||
tag, exist, err = ts.tagCommonRepo.GetTagByID(ctx, tagID, false)
|
||||
if !exist {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -23,8 +23,9 @@ module.exports = {
|
|||
tsconfigRootDir: __dirname,
|
||||
project: ['./tsconfig.json'],
|
||||
},
|
||||
plugins: ['react', '@typescript-eslint'],
|
||||
plugins: ['react', '@typescript-eslint', 'prettier'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'no-unused-vars': 'off',
|
||||
'no-console': 'off',
|
||||
'import/prefer-default-export': 'off',
|
||||
|
@ -50,6 +51,7 @@ module.exports = {
|
|||
'react-hooks/exhaustive-deps': 'off',
|
||||
'react/jsx-props-no-spreading': 'off',
|
||||
'@typescript-eslint/default-param-last': 'off',
|
||||
'no-nested-ternary': 'off',
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{
|
||||
"*.{js,jsx,ts,tsx}": ["eslint --cache --fix"],
|
||||
"*.{js,jsx,less,md,json}": ["prettier --write"],
|
||||
"*.{js,jsx,scss,md,json}": ["prettier --write"],
|
||||
"*.ts?(x)": ["prettier --parser=typescript --write"]
|
||||
}
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
module.exports = {
|
||||
extends: ['@commitlint/routes-conventional'],
|
||||
extends: ['@commitlint/config-conventional'],
|
||||
};
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
||||
"prepare": "cd .. && husky install",
|
||||
"preinstall": "node ./scripts/preinstall.js"
|
||||
"preinstall": "node ./scripts/preinstall.js",
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
"dependencies": {
|
||||
"axios": "^0.27.2",
|
||||
|
@ -19,6 +20,7 @@
|
|||
"codemirror": "5.65.0",
|
||||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"diff": "^5.1.0",
|
||||
"i18next": "^21.9.0",
|
||||
"katex": "^0.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -39,6 +41,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@commitlint/cli": "^17.0.3",
|
||||
"@commitlint/config-conventional": "^17.2.0",
|
||||
"@fullhuman/postcss-purgecss": "^4.1.3",
|
||||
"@testing-library/dom": "^8.17.1",
|
||||
"@testing-library/jest-dom": "^4.2.4",
|
||||
|
|
|
@ -2,6 +2,7 @@ lockfileVersion: 5.4
|
|||
|
||||
specifiers:
|
||||
'@commitlint/cli': ^17.0.3
|
||||
'@commitlint/config-conventional': ^17.2.0
|
||||
'@fullhuman/postcss-purgecss': ^4.1.3
|
||||
'@testing-library/dom': ^8.17.1
|
||||
'@testing-library/jest-dom': ^4.2.4
|
||||
|
@ -25,6 +26,7 @@ specifiers:
|
|||
copy-to-clipboard: ^3.3.2
|
||||
customize-cra: ^1.0.0
|
||||
dayjs: ^1.11.5
|
||||
diff: ^5.1.0
|
||||
eslint: ^8.0.1
|
||||
eslint-config-airbnb: ^19.0.4
|
||||
eslint-config-airbnb-typescript: ^17.0.0
|
||||
|
@ -73,6 +75,7 @@ dependencies:
|
|||
codemirror: 5.65.0
|
||||
copy-to-clipboard: 3.3.2
|
||||
dayjs: 1.11.5
|
||||
diff: 5.1.0
|
||||
i18next: 21.9.2
|
||||
katex: 0.16.2
|
||||
lodash: 4.17.21
|
||||
|
@ -93,6 +96,7 @@ dependencies:
|
|||
|
||||
devDependencies:
|
||||
'@commitlint/cli': 17.1.2
|
||||
'@commitlint/config-conventional': 17.2.0
|
||||
'@fullhuman/postcss-purgecss': 4.1.3_postcss@8.4.16
|
||||
'@testing-library/dom': 8.18.1
|
||||
'@testing-library/jest-dom': 4.2.4
|
||||
|
@ -1421,6 +1425,13 @@ packages:
|
|||
- '@swc/wasm'
|
||||
dev: true
|
||||
|
||||
/@commitlint/config-conventional/17.2.0:
|
||||
resolution: {integrity: sha512-g5hQqRa80f++SYS233dbDSg16YdyounMTAhVcmqtInNeY/GF3aA4st9SVtJxpeGrGmueMrU4L+BBb+6Vs5wrcg==}
|
||||
engines: {node: '>=v14'}
|
||||
dependencies:
|
||||
conventional-changelog-conventionalcommits: 5.0.0
|
||||
dev: true
|
||||
|
||||
/@commitlint/config-validator/17.1.0:
|
||||
resolution: {integrity: sha512-Q1rRRSU09ngrTgeTXHq6ePJs2KrI+axPTgkNYDWSJIuS1Op4w3J30vUfSXjwn5YEJHklK3fSqWNHmBhmTR7Vdg==}
|
||||
engines: {node: '>=v14'}
|
||||
|
@ -3765,6 +3776,15 @@ packages:
|
|||
q: 1.5.1
|
||||
dev: true
|
||||
|
||||
/conventional-changelog-conventionalcommits/5.0.0:
|
||||
resolution: {integrity: sha512-lCDbA+ZqVFQGUj7h9QBKoIpLhl8iihkO0nCTyRNzuXtcd7ubODpYB04IFy31JloiJgG0Uovu8ot8oxRzn7Nwtw==}
|
||||
engines: {node: '>=10'}
|
||||
dependencies:
|
||||
compare-func: 2.0.0
|
||||
lodash: 4.17.21
|
||||
q: 1.5.1
|
||||
dev: true
|
||||
|
||||
/conventional-commits-parser/3.2.4:
|
||||
resolution: {integrity: sha512-nK7sAtfi+QXbxHCYfhpZsfRtaitZLIA6889kFIouLvz6repszQDgxBu7wf2WbU+Dco7sAnNCJYERCwt54WPC2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
@ -4744,6 +4764,11 @@ packages:
|
|||
resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
|
||||
/diff/5.1.0:
|
||||
resolution: {integrity: sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw==}
|
||||
engines: {node: '>=0.3.1'}
|
||||
dev: false
|
||||
|
||||
/dir-glob/3.0.1:
|
||||
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
|
@ -557,3 +557,12 @@ export const TIMEZONES = [
|
|||
},
|
||||
];
|
||||
export const DEFAULT_TIMEZONE = 'UTC+0';
|
||||
|
||||
export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
|
||||
'undeleted',
|
||||
'deleted',
|
||||
'downvote',
|
||||
'upvote',
|
||||
'reopened',
|
||||
'closed',
|
||||
];
|
||||
|
|
|
@ -124,6 +124,7 @@ export interface UserInfoRes extends UserInfoBase {
|
|||
*/
|
||||
mail_status: number;
|
||||
language: string;
|
||||
is_admin: boolean;
|
||||
e_mail?: string;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
@ -373,3 +374,35 @@ export interface AdminDashboard {
|
|||
};
|
||||
};
|
||||
}
|
||||
|
||||
export interface TimelineReq {
|
||||
show_vote: boolean;
|
||||
object_id: string;
|
||||
}
|
||||
|
||||
export interface TimelineItem {
|
||||
activity_id: number;
|
||||
revision_id: number;
|
||||
created_at: number;
|
||||
activity_type: string;
|
||||
username: string;
|
||||
user_display_name: string;
|
||||
comment: string;
|
||||
object_id: string;
|
||||
object_type: string;
|
||||
cancelled: boolean;
|
||||
cancelled_at: any;
|
||||
}
|
||||
|
||||
export interface TimelineObject {
|
||||
title: string;
|
||||
object_type: string;
|
||||
question_id: string;
|
||||
answer_id: string;
|
||||
main_tag_slug_name?: string;
|
||||
}
|
||||
|
||||
export interface TimelineRes {
|
||||
object_info: TimelineObject;
|
||||
timeline: TimelineItem[];
|
||||
}
|
||||
|
|
|
@ -8,6 +8,7 @@ interface Props {
|
|||
data: any;
|
||||
showAvatar?: boolean;
|
||||
avatarSize?: string;
|
||||
showReputation?: boolean;
|
||||
avatarSearchStr?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
@ -18,6 +19,7 @@ const Index: FC<Props> = ({
|
|||
avatarSize = '20px',
|
||||
className = 'fs-14',
|
||||
avatarSearchStr = 's=48',
|
||||
showReputation = true,
|
||||
}) => {
|
||||
return (
|
||||
<div className={`text-secondary ${className}`}>
|
||||
|
@ -47,9 +49,11 @@ const Index: FC<Props> = ({
|
|||
</>
|
||||
)}
|
||||
|
||||
<span className="fw-bold" title="Reputation">
|
||||
{formatCount(data?.rank)}
|
||||
</span>
|
||||
{showReputation && (
|
||||
<span className="fw-bold" title="Reputation">
|
||||
{formatCount(data?.rank)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,98 @@
|
|||
import { FC, memo } from 'react';
|
||||
|
||||
import { Tag } from '@/components';
|
||||
import { diffText } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
objectType: string;
|
||||
newData: Record<string, any>;
|
||||
oldData?: Record<string, any>;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ objectType, newData, oldData, className = '' }) => {
|
||||
if (!newData?.original_text) return null;
|
||||
|
||||
let tag = newData.tags;
|
||||
if (objectType === 'question' && oldData?.tags) {
|
||||
const addTags = newData.tags.filter(
|
||||
(c) => !oldData?.tags?.find((p) => p.slug_name === c.slug_name),
|
||||
);
|
||||
|
||||
let deleteTags = oldData?.tags
|
||||
.filter((c) => !newData?.tags.find((p) => p.slug_name === c.slug_name))
|
||||
.map((v) => ({ ...v, state: 'delete' }));
|
||||
|
||||
deleteTags = deleteTags?.map((v) => {
|
||||
const index = oldData?.tags?.findIndex(
|
||||
(c) => c.slug_name === v.slug_name,
|
||||
);
|
||||
return {
|
||||
...v,
|
||||
pre_index: index,
|
||||
};
|
||||
});
|
||||
|
||||
tag = newData.tags.map((item) => {
|
||||
const find = addTags.find((c) => c.slug_name === item.slug_name);
|
||||
if (find) {
|
||||
return {
|
||||
...find,
|
||||
state: 'add',
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
|
||||
deleteTags.forEach((v) => {
|
||||
tag.splice(v.pre_index, 0, v);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{objectType !== 'answer' && (
|
||||
<h5
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: diffText(newData.title, oldData?.title),
|
||||
}}
|
||||
className="mb-3"
|
||||
/>
|
||||
)}
|
||||
{objectType === 'question' && (
|
||||
<div className="mb-4">
|
||||
{tag.map((item) => {
|
||||
return (
|
||||
<Tag
|
||||
key={item.slug_name}
|
||||
className="me-1"
|
||||
data={item}
|
||||
textClassName={`d-inline-block review-text-${item.state}`}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{objectType === 'tag' && (
|
||||
<div className="mb-4">
|
||||
{`/tags/${
|
||||
newData?.main_tag_slug_name
|
||||
? diffText(
|
||||
newData.main_tag_slug_name,
|
||||
oldData?.main_tag_slug_name,
|
||||
)
|
||||
: diffText(newData.slug_name, oldData?.slug_name)
|
||||
}`}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: diffText(newData.original_text, oldData?.original_text),
|
||||
}}
|
||||
className="pre-line"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -1,13 +1,15 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { FC, memo, ReactNode } from 'react';
|
||||
import { Trans } from 'react-i18next';
|
||||
|
||||
const Index: FC = () => {
|
||||
const Index: FC<{ children?: ReactNode }> = ({ children }) => {
|
||||
return (
|
||||
<div className="text-center py-5">
|
||||
<Trans i18nKey="personal.list_empty">
|
||||
We couldn't find anything. <br /> Try different or less specific
|
||||
keywords.
|
||||
</Trans>
|
||||
{children || (
|
||||
<Trans i18nKey="personal.list_empty">
|
||||
We couldn't find anything. <br /> Try different or less specific
|
||||
keywords.
|
||||
</Trans>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -56,6 +56,12 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
|||
{userInfo?.is_admin ? (
|
||||
<Dropdown.Item href="/admin">{t('header.nav.admin')}</Dropdown.Item>
|
||||
) : null}
|
||||
{/* TODO: use review permission */}
|
||||
{userInfo?.is_admin ? (
|
||||
<Dropdown.Item href="/review">
|
||||
{t('header.nav.review')}
|
||||
</Dropdown.Item>
|
||||
) : null}
|
||||
<Dropdown.Divider />
|
||||
<Dropdown.Item onClick={logOut}>
|
||||
{t('header.nav.logout')}
|
||||
|
|
|
@ -122,9 +122,6 @@ const Header: FC = () => {
|
|||
<NavLink className="nav-link" to="/tags">
|
||||
{t('header.nav.tag')}
|
||||
</NavLink>
|
||||
<NavLink className="nav-link d-none" to="/users">
|
||||
{t('header.nav.user')}
|
||||
</NavLink>
|
||||
</Nav>
|
||||
</Col>
|
||||
<hr className="hr lg-none mt-2" />
|
||||
|
|
|
@ -8,9 +8,15 @@ interface IProps {
|
|||
data: Tag;
|
||||
href?: string;
|
||||
className?: string;
|
||||
textClassName?: string;
|
||||
}
|
||||
|
||||
const Index: FC<IProps> = ({ className = '', href, data }) => {
|
||||
const Index: FC<IProps> = ({
|
||||
data,
|
||||
href,
|
||||
className = '',
|
||||
textClassName = '',
|
||||
}) => {
|
||||
href ||= `/tags/${encodeURIComponent(
|
||||
data.main_tag_slug_name || data.slug_name,
|
||||
)}`.toLowerCase();
|
||||
|
@ -24,7 +30,7 @@ const Index: FC<IProps> = ({ className = '', href, data }) => {
|
|||
data.recommend && 'badge-tag-required',
|
||||
className,
|
||||
)}>
|
||||
{data.slug_name}
|
||||
<span className={textClassName}>{data.slug_name}</span>
|
||||
</a>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -10,10 +10,19 @@ interface Props {
|
|||
data: any;
|
||||
time: number;
|
||||
preFix: string;
|
||||
isLogged: boolean;
|
||||
timelinePath: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
|
||||
const Index: FC<Props> = ({
|
||||
data,
|
||||
time,
|
||||
preFix,
|
||||
isLogged,
|
||||
timelinePath,
|
||||
className = '',
|
||||
}) => {
|
||||
return (
|
||||
<div className={classnames('d-flex', className)}>
|
||||
{data?.status !== 'deleted' ? (
|
||||
|
@ -62,7 +71,18 @@ const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
|
|||
{formatCount(data?.rank)}
|
||||
</span>
|
||||
</div>
|
||||
{time && <FormatTime time={time} preFix={preFix} />}
|
||||
{time &&
|
||||
(isLogged ? (
|
||||
<Link to={timelinePath}>
|
||||
<FormatTime
|
||||
time={time}
|
||||
preFix={preFix}
|
||||
className="link-secondary"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<FormatTime time={time} preFix={preFix} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -27,6 +27,7 @@ import QueryGroup from './QueryGroup';
|
|||
import BrandUpload from './BrandUpload';
|
||||
import SchemaForm, { JSONSchema, UISchema, initFormData } from './SchemaForm';
|
||||
import Labels from './LabelsCard';
|
||||
import DiffContent from './DiffContent';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -60,5 +61,6 @@ export {
|
|||
SchemaForm,
|
||||
initFormData,
|
||||
Labels,
|
||||
DiffContent,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -253,3 +253,33 @@ a {
|
|||
padding: 1px 0.5rem 2px;
|
||||
height: 24px;
|
||||
}
|
||||
|
||||
.review-text-delete {
|
||||
color: #842029;
|
||||
background-color: #f8d7da;
|
||||
text-decoration: line-through;
|
||||
.review-text-add {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
|
||||
.review-text-add {
|
||||
color: #0f5132;
|
||||
background-color: #d1e7dd;
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.rotate-90-deg {
|
||||
display: inline-block;
|
||||
transform: rotate(90deg);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.rotate-0-deg {
|
||||
display: inline-block;
|
||||
transform: rotate(0deg);
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
|
||||
.pre-line {
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
|
|
@ -25,6 +25,7 @@ interface FormDataItem {
|
|||
tags: Type.FormValue<Type.Tag[]>;
|
||||
content: Type.FormValue<string>;
|
||||
answer: Type.FormValue<string>;
|
||||
edit_summary: Type.FormValue<string>;
|
||||
}
|
||||
const initFormData = {
|
||||
title: {
|
||||
|
@ -47,9 +48,15 @@ const initFormData = {
|
|||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
edit_summary: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
};
|
||||
|
||||
const Ask = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'ask' });
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const [checked, setCheckState] = useState(false);
|
||||
const [focusType, setForceType] = useState('');
|
||||
|
@ -63,7 +70,6 @@ const Ask = () => {
|
|||
|
||||
const { qid } = useParams();
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'ask' });
|
||||
|
||||
const isEdit = qid !== undefined;
|
||||
const { data: similarQuestions = { list: [] } } = useQueryQuestionByTitle(
|
||||
|
@ -114,6 +120,15 @@ const Ask = () => {
|
|||
answer: { ...formData.answer, value },
|
||||
});
|
||||
|
||||
const handleSummaryChange = (evt: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
edit_summary: {
|
||||
...formData.edit_summary,
|
||||
value: evt.currentTarget.value,
|
||||
},
|
||||
});
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { title, content, tags, answer } = formData;
|
||||
|
@ -205,7 +220,11 @@ const Ask = () => {
|
|||
tags: formData.tags.value,
|
||||
};
|
||||
if (isEdit) {
|
||||
modifyQuestion({ ...params, id: qid })
|
||||
modifyQuestion({
|
||||
...params,
|
||||
id: qid,
|
||||
edit_summary: formData.edit_summary.value,
|
||||
})
|
||||
.then(() => {
|
||||
navigate(`/questions/${qid}`);
|
||||
})
|
||||
|
@ -354,6 +373,21 @@ const Ask = () => {
|
|||
{formData.tags.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
{isEdit && (
|
||||
<Form.Group controlId="edit_summary" className="my-3">
|
||||
<Form.Label>{t('form.fields.edit_summary.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
defaultValue={formData.edit_summary.value}
|
||||
isInvalid={formData.edit_summary.isInvalid}
|
||||
placeholder={t('form.fields.edit_summary.placeholder')}
|
||||
onChange={handleSummaryChange}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.edit_summary.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
)}
|
||||
{!checked && (
|
||||
<div className="mt-3">
|
||||
<Button type="submit" className="me-2">
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { memo, FC, useEffect, useRef } from 'react';
|
||||
import { Row, Col, Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import {
|
||||
Actions,
|
||||
|
@ -22,12 +23,14 @@ interface Props {
|
|||
/** is author */
|
||||
isAuthor: boolean;
|
||||
questionTitle: string;
|
||||
isLogged: boolean;
|
||||
callback: (type: string) => void;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
aid,
|
||||
data,
|
||||
isAuthor,
|
||||
isLogged,
|
||||
questionTitle = '',
|
||||
callback,
|
||||
}) => {
|
||||
|
@ -119,7 +122,17 @@ const Index: FC<Props> = ({
|
|||
data={data?.update_user_info}
|
||||
time={Number(data.update_time)}
|
||||
preFix={t('edit')}
|
||||
isLogged={isLogged}
|
||||
timelinePath={`/posts/${data.question_id}/${data.id}/timeline`}
|
||||
/>
|
||||
) : isLogged ? (
|
||||
<Link to={`/posts/${data.question_id}/${data.id}/timeline`}>
|
||||
<FormatTime
|
||||
time={Number(data.update_time)}
|
||||
preFix={t('edit')}
|
||||
className="link-secondary fs-14"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<FormatTime
|
||||
time={Number(data.update_time)}
|
||||
|
@ -133,6 +146,8 @@ const Index: FC<Props> = ({
|
|||
data={data?.user_info}
|
||||
time={Number(data.create_time)}
|
||||
preFix={t('answered')}
|
||||
isLogged={isLogged}
|
||||
timelinePath={`/posts/${data.question_id}/${data.id}/timeline`}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -18,10 +18,11 @@ import { following } from '@/services';
|
|||
interface Props {
|
||||
data: any;
|
||||
hasAnswer: boolean;
|
||||
isLogged: boolean;
|
||||
initPage: (type: string) => void;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
||||
const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
|
||||
const { t } = useTranslation('translation', {
|
||||
keyPrefix: 'question_detail',
|
||||
});
|
||||
|
@ -133,7 +134,17 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
data={data?.user_info}
|
||||
time={data.edit_time}
|
||||
preFix={t('edit')}
|
||||
isLogged={isLogged}
|
||||
timelinePath={`/posts/${data.id}/timeline`}
|
||||
/>
|
||||
) : isLogged ? (
|
||||
<Link to={`/posts/${data.id}/timeline`}>
|
||||
<FormatTime
|
||||
time={data.edit_time}
|
||||
preFix={t('edit')}
|
||||
className="link-secondary fs-14"
|
||||
/>
|
||||
</Link>
|
||||
) : (
|
||||
<FormatTime
|
||||
time={data.edit_time}
|
||||
|
@ -147,6 +158,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
|
|||
data={data?.user_info}
|
||||
time={data.create_time}
|
||||
preFix={t('asked')}
|
||||
isLogged={isLogged}
|
||||
timelinePath={`/posts/${data.id}/timeline`}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
|
|
@ -39,6 +39,7 @@ const Index = () => {
|
|||
const { setUsers } = usePageUsers();
|
||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isAuthor = userInfo?.username === question?.user_info?.username;
|
||||
const isLogged = Boolean(userInfo?.access_token);
|
||||
const requestAnswers = async () => {
|
||||
const res = await getAnswers({
|
||||
order: order === 'updated' ? order : 'default',
|
||||
|
@ -126,6 +127,7 @@ const Index = () => {
|
|||
data={question}
|
||||
initPage={initPage}
|
||||
hasAnswer={answers.count > 0}
|
||||
isLogged={isLogged}
|
||||
/>
|
||||
{answers.count > 0 && (
|
||||
<>
|
||||
|
@ -139,6 +141,7 @@ const Index = () => {
|
|||
questionTitle={question?.title || ''}
|
||||
isAuthor={isAuthor}
|
||||
callback={initPage}
|
||||
isLogged={isLogged}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
|
|
|
@ -0,0 +1,74 @@
|
|||
import { FC } from 'react';
|
||||
import {
|
||||
Container,
|
||||
Row,
|
||||
Col,
|
||||
Alert,
|
||||
Badge,
|
||||
Stack,
|
||||
Button,
|
||||
} from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { BaseUserCard, FormatTime, Empty } from '@/components';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
|
||||
|
||||
const { user } = loggedUserInfoStore.getState();
|
||||
return (
|
||||
<Container className="pt-2 mt-4 mb-5">
|
||||
<Row>
|
||||
<Col lg={{ span: 7, offset: 1 }}>
|
||||
<h3 className="mb-4">{t('review')}</h3>
|
||||
<Alert variant="secondary">
|
||||
<Stack className="align-items-start">
|
||||
<Badge bg="secondary" className="mb-2">
|
||||
{t('question_edit')}
|
||||
</Badge>
|
||||
<Link to="/review">
|
||||
How do I test whether variable against multiple
|
||||
</Link>
|
||||
<p className="mb-0">
|
||||
{t('edit_summary')}: Editing part of the code and correcting the
|
||||
grammar.
|
||||
</p>
|
||||
</Stack>
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
gap={1}
|
||||
className="align-items-baseline mt-2">
|
||||
<BaseUserCard data={user} avatarSize="24" />
|
||||
<FormatTime
|
||||
time={Date.now()}
|
||||
className="small text-secondary"
|
||||
preFix={t('proposed')}
|
||||
/>
|
||||
</Stack>
|
||||
</Alert>
|
||||
</Col>
|
||||
<Col lg={{ span: 7, offset: 1 }}>Content</Col>
|
||||
<Col lg={{ span: 7, offset: 1 }}>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<Button variant="outline-primary">
|
||||
{t('approve', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<Button variant="outline-primary">
|
||||
{t('reject', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<Button variant="outline-primary">
|
||||
{t('skip', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Col>
|
||||
<Col lg={{ span: 7, offset: 1 }}>
|
||||
<Empty>{t('empty')}</Empty>
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -12,8 +12,11 @@ import {
|
|||
saveSynonymsTags,
|
||||
deleteTag,
|
||||
} from '@/services';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
|
||||
const TagIntroduction = () => {
|
||||
const userInfo = loggedUserInfoStore((state) => state.user);
|
||||
const isLogged = Boolean(userInfo?.access_token);
|
||||
const [isEdit, setEditState] = useState(false);
|
||||
const { tagName } = useParams();
|
||||
const { data: tagInfo } = useTagInfo({ name: tagName });
|
||||
|
@ -125,6 +128,13 @@ const TagIntroduction = () => {
|
|||
</Button>
|
||||
);
|
||||
})}
|
||||
{isLogged && (
|
||||
<Link
|
||||
to={`/tags/${tagInfo?.tag_id}/timeline`}
|
||||
className="link-secondary btn-no-border p-0 fs-14 ms-3">
|
||||
{t('history')}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">
|
||||
|
|
|
@ -0,0 +1,142 @@
|
|||
import { FC, useState } from 'react';
|
||||
import { Button, Row, Col } from 'react-bootstrap';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon, BaseUserCard, DiffContent, FormatTime } from '@/components';
|
||||
import { TIMELINE_NORMAL_ACTIVITY_TYPE } from '@/common/constants';
|
||||
import * as Type from '@/common/interface';
|
||||
import { getTimelineDetail } from '@/services';
|
||||
|
||||
interface Props {
|
||||
data: Type.TimelineItem;
|
||||
objectInfo: Type.TimelineObject;
|
||||
isAdmin: boolean;
|
||||
revisionList: Type.TimelineItem[];
|
||||
}
|
||||
const Index: FC<Props> = ({ data, isAdmin, objectInfo, revisionList }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [detailData, setDetailData] = useState({
|
||||
new_revision: {},
|
||||
old_revision: {},
|
||||
});
|
||||
|
||||
const handleItemClick = async (id) => {
|
||||
if (!isOpen) {
|
||||
const revisionItem = revisionList?.find((v) => v.revision_id === id);
|
||||
let oldId;
|
||||
if (revisionList?.length > 0 && revisionItem) {
|
||||
const idIndex = revisionList.indexOf(revisionItem) || 0;
|
||||
if (idIndex === revisionList.length - 1) {
|
||||
oldId = 0;
|
||||
} else {
|
||||
oldId = revisionList[idIndex + 1].revision_id;
|
||||
}
|
||||
}
|
||||
const res = await getTimelineDetail({
|
||||
new_revision_id: id,
|
||||
old_revision_id: oldId,
|
||||
});
|
||||
setDetailData(res);
|
||||
}
|
||||
setIsOpen(!isOpen);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<tr>
|
||||
<td>
|
||||
<FormatTime time={data.created_at} />
|
||||
<br />
|
||||
{data.cancelled_at > 0 && <FormatTime time={data.cancelled_at} />}
|
||||
</td>
|
||||
<td>
|
||||
{(data.activity_type === 'rollback' ||
|
||||
data.activity_type === 'edited' ||
|
||||
data.activity_type === 'asked' ||
|
||||
data.activity_type === 'created' ||
|
||||
(objectInfo.object_type === 'answer' &&
|
||||
data.activity_type === 'answered')) && (
|
||||
<Button
|
||||
onClick={() => handleItemClick(data.revision_id)}
|
||||
variant="link"
|
||||
className="text-body p-0 btn-no-border">
|
||||
<Icon
|
||||
name="caret-right-fill"
|
||||
className={`me-1 ${isOpen ? 'rotate-90-deg' : 'rotate-0-deg'}`}
|
||||
/>
|
||||
{t(data.activity_type)}
|
||||
</Button>
|
||||
)}
|
||||
{data.activity_type === 'accept' && (
|
||||
<Link to={`/questions/${objectInfo.question_id}`}>
|
||||
{t(data.activity_type)}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{objectInfo.object_type === 'question' &&
|
||||
data.activity_type === 'answered' && (
|
||||
<Link
|
||||
to={`/questions/${objectInfo.question_id}/${data.object_id}`}>
|
||||
{t(data.activity_type)}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{data.activity_type === 'commented' && (
|
||||
<Link
|
||||
to={
|
||||
objectInfo.object_type === 'answer'
|
||||
? `/questions/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}`
|
||||
: `/questions/${objectInfo.question_id}?commentId=${data.object_id}`
|
||||
}>
|
||||
{t(data.activity_type)}
|
||||
</Link>
|
||||
)}
|
||||
|
||||
{TIMELINE_NORMAL_ACTIVITY_TYPE.includes(data.activity_type) && (
|
||||
<div>{t(data.activity_type)}</div>
|
||||
)}
|
||||
|
||||
{data.cancelled && (
|
||||
<div className="text-danger">{t('cancelled')}</div>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
{data.activity_type === 'downvote' && !isAdmin ? (
|
||||
<div>{t('n_or_a')}</div>
|
||||
) : (
|
||||
<BaseUserCard
|
||||
className="fs-normal"
|
||||
data={{
|
||||
username: data.username,
|
||||
display_name: data.user_display_name,
|
||||
}}
|
||||
showAvatar={false}
|
||||
showReputation={false}
|
||||
/>
|
||||
)}
|
||||
</td>
|
||||
<td>
|
||||
<div dangerouslySetInnerHTML={{ __html: data.comment }} />
|
||||
</td>
|
||||
</tr>
|
||||
<tr className={isOpen ? '' : 'd-none'}>
|
||||
{/* <td /> */}
|
||||
<td colSpan={5} className="p-0 py-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col xxl={8}>
|
||||
<DiffContent
|
||||
objectType={objectInfo.object_type}
|
||||
newData={detailData?.new_revision}
|
||||
oldData={detailData?.old_revision}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -0,0 +1,103 @@
|
|||
import { FC, useState } from 'react';
|
||||
import { Container, Row, Col, Form, Table } from 'react-bootstrap';
|
||||
import { Link, useParams } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { useTimelineData } from '@/services';
|
||||
import { PageTitle, Empty } from '@/components';
|
||||
|
||||
import HistoryItem from './components/Item';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
|
||||
const { qid = '', aid = '', tid = '' } = useParams();
|
||||
const { is_admin } = loggedUserInfoStore((state) => state.user);
|
||||
const [showVotes, setShowVotes] = useState(false);
|
||||
|
||||
const { data: timelineData, isLoading } = useTimelineData({
|
||||
object_id: tid || aid || qid,
|
||||
show_vote: showVotes,
|
||||
});
|
||||
|
||||
const handleSwitch = (bol: boolean) => {
|
||||
setShowVotes(bol);
|
||||
};
|
||||
|
||||
let linkUrl = '';
|
||||
if (timelineData?.object_info.object_type === 'question') {
|
||||
linkUrl = `/questions/${timelineData?.object_info.question_id}`;
|
||||
}
|
||||
|
||||
if (timelineData?.object_info.object_type === 'answer') {
|
||||
linkUrl = `/questions/${timelineData?.object_info.question_id}/${timelineData?.object_info.answer_id}`;
|
||||
}
|
||||
|
||||
if (timelineData?.object_info.object_type === 'tag') {
|
||||
linkUrl = `/tags/${
|
||||
timelineData?.object_info.main_tag_slug_name ||
|
||||
timelineData?.object_info.title
|
||||
}`;
|
||||
}
|
||||
|
||||
const revisionList =
|
||||
timelineData?.timeline?.filter((item) => item.revision_id > 0) || [];
|
||||
|
||||
return (
|
||||
<Container className="py-3">
|
||||
<PageTitle
|
||||
title={
|
||||
timelineData?.object_info.object_type === 'tag'
|
||||
? `${t('title_for_tag')} ${timelineData?.object_info.title}`
|
||||
: `${t('title_for')} ${timelineData?.object_info.title}`
|
||||
}
|
||||
/>
|
||||
<Row className="py-3 justify-content-center">
|
||||
<Col xxl={10}>
|
||||
<h5 className="mb-4">
|
||||
{t('title')}{' '}
|
||||
<Link to={linkUrl}>{timelineData?.object_info?.title}</Link>
|
||||
</h5>
|
||||
{timelineData?.object_info.object_type !== 'tag' && (
|
||||
<Form.Check
|
||||
className="mb-4"
|
||||
type="switch"
|
||||
id="custom-switch"
|
||||
label={t('show_votes')}
|
||||
checked={showVotes}
|
||||
onChange={(e) => handleSwitch(e.target.checked)}
|
||||
/>
|
||||
)}
|
||||
<Table hover>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '20%' }}>{t('datetime')}</th>
|
||||
<th style={{ width: '15%' }}>{t('type')}</th>
|
||||
<th style={{ width: '19%' }}>{t('by')}</th>
|
||||
<th>{t('comment')}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{timelineData?.timeline?.map((item) => {
|
||||
return (
|
||||
<HistoryItem
|
||||
data={item}
|
||||
objectInfo={timelineData?.object_info}
|
||||
key={item.activity_id}
|
||||
isAdmin={is_admin}
|
||||
revisionList={revisionList}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</Table>
|
||||
{!isLoading && Number(timelineData?.timeline?.length) <= 0 && (
|
||||
<Empty>{t('no_data')}</Empty>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</Container>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
|
@ -83,7 +83,7 @@ const routes: RouteNode[] = [
|
|||
return guard.activated();
|
||||
},
|
||||
},
|
||||
// users
|
||||
// for users
|
||||
{
|
||||
path: 'users/:username',
|
||||
page: 'pages/Users/Personal',
|
||||
|
@ -191,6 +191,27 @@ const routes: RouteNode[] = [
|
|||
return guard.forbidden();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/posts/:qid/timeline',
|
||||
page: 'pages/Timeline',
|
||||
guard: async () => {
|
||||
return guard.logged();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/posts/:qid/:aid/timeline',
|
||||
page: 'pages/Timeline',
|
||||
guard: async () => {
|
||||
return guard.logged();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/tags/:tid/timeline',
|
||||
page: 'pages/Timeline',
|
||||
guard: async () => {
|
||||
return guard.logged();
|
||||
},
|
||||
},
|
||||
// for admin
|
||||
{
|
||||
path: 'admin',
|
||||
|
@ -254,6 +275,11 @@ const routes: RouteNode[] = [
|
|||
},
|
||||
],
|
||||
},
|
||||
// for review
|
||||
{
|
||||
path: 'review',
|
||||
page: 'pages/Review',
|
||||
},
|
||||
{
|
||||
path: '*',
|
||||
page: 'pages/404',
|
||||
|
|
|
@ -6,3 +6,4 @@ export * from './search';
|
|||
export * from './tag';
|
||||
export * from './settings';
|
||||
export * from './legal';
|
||||
export * from './timeline';
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
import useSWR from 'swr';
|
||||
import qs from 'qs';
|
||||
|
||||
import request from '@/utils/request';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
export const useTimelineData = (params: Type.TimelineReq) => {
|
||||
const apiUrl = '/answer/api/v1/activity/timeline';
|
||||
const { data, error, mutate } = useSWR<Type.TimelineRes, Error>(
|
||||
`${apiUrl}?${qs.stringify(params, { skipNulls: true })}`,
|
||||
request.instance.get,
|
||||
);
|
||||
return {
|
||||
data,
|
||||
isLoading: !data && !error,
|
||||
error,
|
||||
mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const getTimelineDetail = (params: {
|
||||
new_revision_id: string;
|
||||
old_revision_id: string;
|
||||
}) => {
|
||||
return request.get(
|
||||
`/answer/api/v1/activity/timeline/detail?${qs.stringify(params, {
|
||||
skipNulls: true,
|
||||
})}`,
|
||||
);
|
||||
};
|
|
@ -73,7 +73,7 @@ export const useQueryAnswerInfo = (id: string) => {
|
|||
};
|
||||
|
||||
export const modifyQuestion = (
|
||||
params: Type.QuestionParams & { id: string },
|
||||
params: Type.QuestionParams & { id: string; edit_summary: string },
|
||||
) => {
|
||||
return request.put(`/answer/api/v1/question`, params);
|
||||
};
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
// jest-dom adds custom jest matchers for asserting on DOM nodes.
|
||||
// allows you to do things like:
|
||||
// expect(element).toHaveTextContent(/react/i)
|
||||
// learn more: https://github.com/testing-library/jest-dom
|
||||
import '@testing-library/jest-dom';
|
|
@ -26,6 +26,7 @@ const initUser: UserInfoRes = {
|
|||
status: '',
|
||||
mail_status: 1,
|
||||
language: 'Default',
|
||||
is_admin: false,
|
||||
};
|
||||
|
||||
const loggedUserInfoStore = create<UserInfoStore>((set) => ({
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import i18next from 'i18next';
|
||||
|
||||
const Diff = require('diff');
|
||||
|
||||
function thousandthDivision(num) {
|
||||
const reg = /\d{1,3}(?=(\d{3})+$)/g;
|
||||
return `${num}`.replace(reg, '$&,');
|
||||
|
@ -163,6 +165,34 @@ function handleFormError(
|
|||
return data;
|
||||
}
|
||||
|
||||
function diffText(newText: string, oldText: string): string {
|
||||
if (!newText) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!oldText) {
|
||||
return newText
|
||||
?.replace(/\n/gi, '<br>')
|
||||
?.replace(/<iframe/gi, '<iframe')
|
||||
?.replace(/<input/gi, '<input');
|
||||
}
|
||||
const diff = Diff.diffChars(oldText, newText);
|
||||
const result = diff.map((part) => {
|
||||
if (part.added) {
|
||||
return `<span class="review-text-add">${part.value}</span>`;
|
||||
}
|
||||
if (part.removed) {
|
||||
return `<span class="review-text-delete">${part.value}</span>`;
|
||||
}
|
||||
return part.value;
|
||||
});
|
||||
|
||||
return result
|
||||
.join('')
|
||||
?.replace(/<iframe/gi, '<iframe')
|
||||
?.replace(/<input/gi, '<input');
|
||||
}
|
||||
|
||||
export {
|
||||
thousandthDivision,
|
||||
formatCount,
|
||||
|
@ -175,4 +205,5 @@ export {
|
|||
colorRgb,
|
||||
labelStyle,
|
||||
handleFormError,
|
||||
diffText,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue