Merge remote-tracking branch 'origin/release/1.7.0'
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
.PHONY: build clean ui
|
||||
|
||||
VERSION=1.6.0
|
||||
VERSION=1.7.0
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
||||
@@ -23,7 +23,7 @@ To learn more about the project, visit [answer.apache.org](https://answer.apache
|
||||
### Running with docker
|
||||
|
||||
```bash
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.6.0
|
||||
docker run -d -p 9080:80 -v answer-data:/data --name answer apache/answer:1.7.0
|
||||
```
|
||||
|
||||
For more information, see [Installation](https://answer.apache.org/docs/installation).
|
||||
|
||||
+50
-15
@@ -20,11 +20,13 @@
|
||||
package answercmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/apache/answer/internal/base/conf"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/internal/cli"
|
||||
"github.com/apache/answer/internal/install"
|
||||
"github.com/apache/answer/internal/migrations"
|
||||
@@ -53,6 +55,10 @@ var (
|
||||
i18nSourcePath string
|
||||
// i18nTargetPath i18n to path
|
||||
i18nTargetPath string
|
||||
// resetPasswordEmail user email for password reset
|
||||
resetPasswordEmail string
|
||||
// resetPasswordPassword new password for password reset
|
||||
resetPasswordPassword string
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -76,7 +82,10 @@ func init() {
|
||||
|
||||
i18nCmd.Flags().StringVarP(&i18nTargetPath, "target", "t", "", "i18n target path, eg: -t ./i18n/target")
|
||||
|
||||
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd} {
|
||||
resetPasswordCmd.Flags().StringVarP(&resetPasswordEmail, "email", "e", "", "user email address")
|
||||
resetPasswordCmd.Flags().StringVarP(&resetPasswordPassword, "password", "p", "", "new password (not recommended, will be recorded in shell history)")
|
||||
|
||||
for _, cmd := range []*cobra.Command{initCmd, checkCmd, runCmd, dumpCmd, upgradeCmd, buildCmd, pluginCmd, configCmd, i18nCmd, resetPasswordCmd} {
|
||||
rootCmd.AddCommand(cmd)
|
||||
}
|
||||
}
|
||||
@@ -96,8 +105,8 @@ To run answer, use:
|
||||
Short: "Run Answer",
|
||||
Long: `Start running Answer`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
fmt.Println("config file path: ", cli.GetConfigFilePath())
|
||||
path.FormatAllPath(dataDirPath)
|
||||
fmt.Println("config file path: ", path.GetConfigFilePath())
|
||||
fmt.Println("Answer is starting..........................")
|
||||
runApp()
|
||||
},
|
||||
@@ -111,10 +120,10 @@ To run answer, use:
|
||||
// check config file and database. if config file exists and database is already created, init done
|
||||
cli.InstallAllInitialEnvironment(dataDirPath)
|
||||
|
||||
configFileExist := cli.CheckConfigFile(cli.GetConfigFilePath())
|
||||
configFileExist := cli.CheckConfigFile(path.GetConfigFilePath())
|
||||
if configFileExist {
|
||||
fmt.Println("config file exists, try to read the config...")
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
@@ -128,7 +137,7 @@ To run answer, use:
|
||||
}
|
||||
|
||||
// start installation server to install
|
||||
install.Run(cli.GetConfigFilePath())
|
||||
install.Run(path.GetConfigFilePath())
|
||||
},
|
||||
}
|
||||
|
||||
@@ -138,9 +147,9 @@ To run answer, use:
|
||||
Long: `Upgrade Answer to the latest version`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
log.SetLogger(log.NewStdLogger(os.Stdout))
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
path.FormatAllPath(dataDirPath)
|
||||
cli.InstallI18nBundle(true)
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
@@ -159,8 +168,8 @@ To run answer, use:
|
||||
Long: `Back up database into an SQL file`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
fmt.Println("Answer is backing up data")
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
path.FormatAllPath(dataDirPath)
|
||||
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
@@ -179,9 +188,9 @@ To run answer, use:
|
||||
Short: "Check the required environment",
|
||||
Long: `Check if the current environment meets the startup requirements`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
path.FormatAllPath(dataDirPath)
|
||||
fmt.Println("Start checking the required environment...")
|
||||
if cli.CheckConfigFile(cli.GetConfigFilePath()) {
|
||||
if cli.CheckConfigFile(path.GetConfigFilePath()) {
|
||||
fmt.Println("config file exists [✔]")
|
||||
} else {
|
||||
fmt.Println("config file not exists [x]")
|
||||
@@ -193,7 +202,7 @@ To run answer, use:
|
||||
fmt.Println("upload directory not exists [x]")
|
||||
}
|
||||
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
@@ -246,9 +255,9 @@ To run answer, use:
|
||||
Short: "Set some config to default value",
|
||||
Long: `Set some config to default value`,
|
||||
Run: func(_ *cobra.Command, _ []string) {
|
||||
cli.FormatAllPath(dataDirPath)
|
||||
path.FormatAllPath(dataDirPath)
|
||||
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
||||
if err != nil {
|
||||
fmt.Println("read config failed: ", err.Error())
|
||||
return
|
||||
@@ -297,6 +306,32 @@ To run answer, use:
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
resetPasswordCmd = &cobra.Command{
|
||||
Use: "passwd",
|
||||
Aliases: []string{"password", "reset-password"},
|
||||
Short: "Reset user password",
|
||||
Long: "Reset user password by email address.",
|
||||
Example: ` # Interactive mode (recommended, safest)
|
||||
answer passwd -C ./answer-data
|
||||
|
||||
# Specify email only (will prompt for password securely)
|
||||
answer passwd -C ./answer-data --email user@example.com
|
||||
answer passwd -C ./answer-data -e user@example.com
|
||||
|
||||
# Specify email and password (NOT recommended, will be recorded in shell history)
|
||||
answer passwd -C ./answer-data -e user@example.com -p newpassword123`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
opts := &cli.ResetPasswordOptions{
|
||||
Email: resetPasswordEmail,
|
||||
Password: resetPasswordPassword,
|
||||
}
|
||||
if err := cli.ResetPassword(context.Background(), dataDirPath, opts); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
// Execute adds all child commands to the root command and sets flags appropriately.
|
||||
|
||||
+2
-2
@@ -28,7 +28,7 @@ import (
|
||||
"github.com/apache/answer/internal/base/conf"
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/cron"
|
||||
"github.com/apache/answer/internal/cli"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman"
|
||||
@@ -67,7 +67,7 @@ func Main() {
|
||||
}
|
||||
|
||||
func runApp() {
|
||||
c, err := conf.ReadConfig(cli.GetConfigFilePath())
|
||||
c, err := conf.ReadConfig(path.GetConfigFilePath())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
+7
-26
@@ -1,28 +1,8 @@
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Code generated by Wire. DO NOT EDIT.
|
||||
|
||||
//go:generate go run github.com/google/wire/cmd/wire
|
||||
//go:build !wireinject
|
||||
// +build !wireinject
|
||||
|
||||
package answercmd
|
||||
|
||||
@@ -192,22 +172,22 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
objService := object_info.NewObjService(answerRepo, questionRepo, commentCommonRepo, tagCommonRepo, tagCommonService)
|
||||
notificationQueueService := notice_queue.NewNotificationQueueService()
|
||||
externalNotificationQueueService := notice_queue.NewNewQuestionNotificationQueueService()
|
||||
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, externalNotificationQueueService, activityQueueService, eventQueueService)
|
||||
rolePowerRelRepo := role.NewRolePowerRelRepo(dataData)
|
||||
rolePowerRelService := role2.NewRolePowerRelService(rolePowerRelRepo, userRoleRelService)
|
||||
rankService := rank2.NewRankService(userCommon, userRankRepo, objService, userRoleRelService, rolePowerRelService, configService)
|
||||
limitRepo := limit.NewRateLimitRepo(dataData)
|
||||
rateLimitMiddleware := middleware.NewRateLimitMiddleware(limitRepo)
|
||||
commentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware)
|
||||
reportRepo := report.NewReportRepo(dataData, uniqueIDRepo)
|
||||
tagService := tag2.NewTagService(tagRepo, tagCommonService, revisionService, followRepo, siteInfoCommonService, activityQueueService)
|
||||
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
|
||||
answerActivityService := activity2.NewAnswerActivityService(answerActivityRepo, configService)
|
||||
externalNotificationService := notification.NewExternalNotificationService(dataData, userNotificationConfigRepo, followRepo, emailService, userRepo, externalNotificationQueueService, userExternalLoginRepo, siteInfoCommonService)
|
||||
reviewRepo := review.NewReviewRepo(dataData)
|
||||
reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalNotificationQueueService, tagCommonService, questionCommon, notificationQueueService, siteInfoCommonService)
|
||||
reviewService := review2.NewReviewService(reviewRepo, objService, userCommon, userRepo, questionRepo, answerRepo, userRoleRelService, externalNotificationQueueService, tagCommonService, questionCommon, notificationQueueService, siteInfoCommonService, commentCommonRepo)
|
||||
questionService := content.NewQuestionService(activityRepo, questionRepo, answerRepo, tagCommonService, tagService, questionCommon, userCommon, userRepo, userRoleRelService, revisionService, metaCommonService, collectionCommon, answerActivityService, emailService, notificationQueueService, externalNotificationQueueService, activityQueueService, siteInfoCommonService, externalNotificationService, reviewService, configService, eventQueueService, reviewRepo)
|
||||
answerService := content.NewAnswerService(answerRepo, questionRepo, questionCommon, userCommon, collectionCommon, userRepo, revisionService, answerActivityService, answerCommon, voteRepo, emailService, userRoleRelService, notificationQueueService, externalNotificationQueueService, activityQueueService, reviewService, eventQueueService)
|
||||
commentService := comment2.NewCommentService(commentRepo, commentCommonRepo, userCommon, objService, voteRepo, emailService, userRepo, notificationQueueService, externalNotificationQueueService, activityQueueService, eventQueueService, reviewService)
|
||||
commentController := controller.NewCommentController(commentService, rankService, captchaService, rateLimitMiddleware)
|
||||
reportHandle := report_handle.NewReportHandle(questionService, answerService, commentService)
|
||||
reportService := report2.NewReportService(reportRepo, objService, userCommon, answerRepo, questionRepo, commentCommonRepo, reportHandle, configService, eventQueueService)
|
||||
reportController := controller.NewReportController(reportService, rankService, captchaService)
|
||||
@@ -289,7 +269,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
||||
captchaController := controller.NewCaptchaController()
|
||||
embedController := controller.NewEmbedController()
|
||||
renderController := controller.NewRenderController()
|
||||
pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController, renderController)
|
||||
sidebarController := controller.NewSidebarController()
|
||||
pluginAPIRouter := router.NewPluginAPIRouter(connectorController, userCenterController, captchaController, embedController, renderController, sidebarController)
|
||||
ginEngine := server.NewHTTPServer(debug, staticRouter, answerAPIRouter, swaggerRouter, uiRouter, authUserMiddleware, avatarMiddleware, shortIDMiddleware, templateRouter, pluginAPIRouter, uiConf)
|
||||
scheduledTaskManager := cron.NewScheduledTaskManager(siteInfoCommonService, questionService, fileRecordService, userAdminService, serviceConf)
|
||||
application := newApplication(serverConf, ginEngine, scheduledTaskManager)
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Package docs Code generated by swaggo/swag. DO NOT EDIT
|
||||
package docs
|
||||
|
||||
|
||||
@@ -1,20 +1,3 @@
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
|
||||
basePath: /
|
||||
definitions:
|
||||
constant.NotificationChannelKey:
|
||||
|
||||
@@ -60,6 +60,7 @@ require (
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/image v0.20.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/term v0.30.0
|
||||
golang.org/x/text v0.23.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
|
||||
@@ -785,6 +785,8 @@ golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXR
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username up to 30 characters
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username up to 30 characters
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username up to 30 characters
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username up to 30 characters
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username up to 30 characters
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Man kan nævne dig som "@username".
|
||||
msg: Brugernavn skal udfyldes.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Skal bruge tegnsættet "a-z", "0-9", " - . _"'
|
||||
character: 'Skal bruge tegnsættet "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profilbillede
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Leute können dich als "@Benutzername" erwähnen.
|
||||
msg: Benutzername darf nicht leer sein.
|
||||
msg_range: Der Benutzername muss zwischen 2 und 30 Zeichen lang sein.
|
||||
character: 'Muss den Zeichensatz "a-z", "0-9", " - . _" verwenden'
|
||||
character: 'Muss den Zeichensatz "a-z", "0-9", "- . _" verwenden'
|
||||
avatar:
|
||||
label: Profilbild
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+22
-5
@@ -235,6 +235,8 @@ backend:
|
||||
other: No permission to update.
|
||||
content_cannot_empty:
|
||||
other: Content cannot be empty.
|
||||
content_less_than_minumum:
|
||||
other: Not enough content entered.
|
||||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: Reputation rank fail to meet the condition.
|
||||
@@ -264,6 +266,8 @@ backend:
|
||||
other: You cannot delete a tag that is in use.
|
||||
cannot_set_synonym_as_itself:
|
||||
other: You cannot set the synonym of the current tag as itself.
|
||||
minimum_count:
|
||||
other: Not enough tags were entered.
|
||||
smtp:
|
||||
config_from_name_cannot_be_email:
|
||||
other: The from name cannot be a email address.
|
||||
@@ -852,6 +856,7 @@ ui:
|
||||
http_50X: HTTP Error 500
|
||||
http_403: HTTP Error 403
|
||||
logout: Log Out
|
||||
posts: Posts
|
||||
notifications:
|
||||
title: Notifications
|
||||
inbox: Inbox
|
||||
@@ -1158,6 +1163,9 @@ ui:
|
||||
label: Body
|
||||
msg:
|
||||
empty: Body cannot be empty.
|
||||
hint:
|
||||
optional_body: Share what the question is about.
|
||||
minimum_characters: "Share what the question is about, at least {{min_content_length}} characters are required."
|
||||
tags:
|
||||
label: Tags
|
||||
msg:
|
||||
@@ -1179,7 +1187,9 @@ ui:
|
||||
add_btn: Add tag
|
||||
create_btn: Create new tag
|
||||
search_tag: Search tag
|
||||
hint: "Describe what your content is about, at least one tag is required."
|
||||
hint: Describe what your content is about, at least one tag is required.
|
||||
hint_zero_tags: Describe what your content is about.
|
||||
hint_more_than_one_tag: "Describe what your content is about, at least {{min_tags_number}} tags are required."
|
||||
no_result: No tags matched
|
||||
tag_required_text: Required tag (at least one)
|
||||
header:
|
||||
@@ -1235,7 +1245,7 @@ ui:
|
||||
msg:
|
||||
empty: Name cannot be empty.
|
||||
range: Name must be between 2 to 30 characters in length.
|
||||
character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
email:
|
||||
label: Email
|
||||
msg:
|
||||
@@ -1317,7 +1327,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: Gravatar
|
||||
@@ -1407,9 +1417,11 @@ ui:
|
||||
search: Search people
|
||||
question_detail:
|
||||
action: Action
|
||||
created: Created
|
||||
Asked: Asked
|
||||
asked: asked
|
||||
update: Modified
|
||||
Edited: Edited
|
||||
edit: edited
|
||||
commented: commented
|
||||
Views: Viewed
|
||||
@@ -1730,7 +1742,7 @@ ui:
|
||||
admin_name:
|
||||
label: Name
|
||||
msg: Name cannot be empty.
|
||||
character: 'Must use the character set "a-z", "A-Z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
msg_max_length: Name must be between 2 to 30 characters in length.
|
||||
admin_password:
|
||||
label: Password
|
||||
@@ -2118,10 +2130,16 @@ ui:
|
||||
ask_before_display: Ask before displaying external content
|
||||
write:
|
||||
page_title: Write
|
||||
min_content:
|
||||
label: Minimum question body length
|
||||
text: Minimum allowed question body length in characters.
|
||||
restrict_answer:
|
||||
title: Answer write
|
||||
label: Each user can only write one answer for the same question
|
||||
text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused."
|
||||
min_tags:
|
||||
label: "Minimum tags per question"
|
||||
text: "Minimum number of tags required in a question."
|
||||
recommend_tags:
|
||||
label: Recommend tags
|
||||
text: "Recommend tags will show in the dropdown list by default."
|
||||
@@ -2277,7 +2295,6 @@ ui:
|
||||
btn_submit: Save
|
||||
not_found_props: "Required property {{ key }} not found."
|
||||
select: Select
|
||||
|
||||
page_review:
|
||||
review: Review
|
||||
proposed: proposed
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: La gente puede mencionarte con "@nombredeusuario".
|
||||
msg: El nombre de usuario no puede estar vacío.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Debe usar el conjunto de caracteres "a-z", "0-9", " - . _"'
|
||||
character: 'Debe usar el conjunto de caracteres "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Imagen de perfil
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: دیگران میتوانند به شما به بصورت "@username" اشاره کنند.
|
||||
msg: نام کاربری نمی تواند خالی باشد.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'باید از حروف "a-z", "0-9", " - . _" استفاده شود'
|
||||
character: 'باید از حروف "a-z", "0-9", "- . _" استفاده شود'
|
||||
avatar:
|
||||
label: عکس پروفایل
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Les gens peuvent vous mentionner avec "@username".
|
||||
msg: Le nom d'utilisateur ne peut pas être vide.
|
||||
msg_range: Le nom d'utilisateur doit contenir entre 2 et 30 caractères.
|
||||
character: 'Doit utiliser seulement les caractères "a-z", "0-9", " - . _"'
|
||||
character: 'Doit utiliser seulement les caractères "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Photo de profil
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username up to 30 characters
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Gli altri utenti possono menzionarti con @{{username}}.
|
||||
msg: Il nome utente non può essere vuoto.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'È necessario utilizzare il set di caratteri "a-z", "0-9", " - . _"'
|
||||
character: 'È necessario utilizzare il set di caratteri "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Immagine del profilo
|
||||
gravatar: Gravatar
|
||||
|
||||
+2
-2
@@ -1291,7 +1291,7 @@ ui:
|
||||
caption: ユーザーは "@username" としてあなたをメンションできます。
|
||||
msg: ユーザー名は空にできません。
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: '文字セット "a-z", "0-9", " - . _" を使用してください。'
|
||||
character: '文字セット "a-z", "0-9", "- . _" を使用してください。'
|
||||
avatar:
|
||||
label: プロフィール画像
|
||||
gravatar: Gravatar
|
||||
@@ -1345,7 +1345,7 @@ ui:
|
||||
new_pass:
|
||||
label: 新しいパスワード
|
||||
pass_confirm:
|
||||
label: 新しいパスワードの確認
|
||||
label: 新しいパスワードの確認
|
||||
interface:
|
||||
heading: 外観
|
||||
lang:
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: 다른 사용자가 "@사용자이름"으로 멘션할 수 있습니다.
|
||||
msg: 사용자 이름을 입력하세요.
|
||||
msg_range: 유저 이름은 2-30 자 길이여야 합니다.
|
||||
character: '문자 집합 "a-z", "0-9", " - . _"을 사용해야 합니다.'
|
||||
character: '문자 집합 "a-z", "0-9", "- . _"을 사용해야 합니다.'
|
||||
avatar:
|
||||
label: 프로필 이미지
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Ludzie mogą oznaczać Cię jako "@nazwa_użytkownika".
|
||||
msg: Nazwa użytkownika nie może być pusta.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Należy używać zestawu znaków "a-z", "0-9", " - . _"'
|
||||
character: 'Należy używać zestawu znaków "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Zdjęcie profilowe
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -683,7 +683,7 @@ ui:
|
||||
caption: As pessoas poderão mensionar você com "@usuário".
|
||||
msg: Nome de usuário não pode ser vazio.
|
||||
msg_range: Nome de usuário até 30 caracteres.
|
||||
character: 'Deve usar o conjunto de caracteres "a-z", "0-9", " - . _"'
|
||||
character: 'Deve usar o conjunto de caracteres "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Perfil Imagem
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: As pessoas poderão mensionar você com "@usuário".
|
||||
msg: Nome de usuário não pode ser vazio.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Deve usar o conjunto de caracteres "a-z", "0-9", " - . _"'
|
||||
character: 'Deve usar o conjunto de caracteres "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Perfil Imagem
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Oamenii te pot menționa ca "@utilizator".
|
||||
msg: Numele de utilizator nu poate fi gol.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Trebuie să utilizați setul de caractere "a-z", "0-9", " - . _"'
|
||||
character: 'Trebuie să utilizați setul de caractere "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Imaginea de profil
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Люди могут упоминать вас как "@username".
|
||||
msg: Имя пользователя не может быть пустым.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Необходимо использовать набор символов "a-z", "0-9", " - . _"'
|
||||
character: 'Необходимо использовать набор символов "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Изображение профиля
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -677,7 +677,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username up to 30 characters
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profilbild
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: People can mention you as "@username".
|
||||
msg: Username cannot be empty.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
character: 'Must use the character set "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: Gravatar
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: Mọi người có thể nhắc đến bạn với "@username".
|
||||
msg: Tên người dùng không thể trống.
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: 'Chỉ sử dụng bộ ký tự "a-z", "0-9", " - . _"'
|
||||
character: 'Chỉ sử dụng bộ ký tự "a-z", "0-9", "- . _"'
|
||||
avatar:
|
||||
label: Hình ảnh hồ sơ
|
||||
gravatar: Gravatar
|
||||
|
||||
+6
-3
@@ -841,6 +841,7 @@ ui:
|
||||
http_50X: HTTP 错误 500
|
||||
http_403: HTTP 错误 403
|
||||
logout: 退出
|
||||
posts: 帖子
|
||||
notifications:
|
||||
title: 通知
|
||||
inbox: 收件箱
|
||||
@@ -1214,7 +1215,7 @@ ui:
|
||||
msg:
|
||||
empty: 名字不能为空
|
||||
range: 名称长度必须在 2 至 30 个字符之间。
|
||||
character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成'
|
||||
character: '只能由 "a-z"、"0-9"、" - . _" 组成'
|
||||
email:
|
||||
label: 邮箱
|
||||
msg:
|
||||
@@ -1292,7 +1293,7 @@ ui:
|
||||
caption: 用户可以通过 "@用户名" 来提及你。
|
||||
msg: 用户名不能为空
|
||||
msg_range: 显示名称长度必须为 2-30 个字符。
|
||||
character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成'
|
||||
character: '只能由 "a-z"、"0-9"、"- . _" 组成'
|
||||
avatar:
|
||||
label: 头像
|
||||
gravatar: Gravatar
|
||||
@@ -1381,9 +1382,11 @@ ui:
|
||||
search: 搜索人员
|
||||
question_detail:
|
||||
action: 操作
|
||||
created: 创建于
|
||||
Asked: 提问于
|
||||
asked: 提问于
|
||||
update: 修改于
|
||||
Edited: 编辑于
|
||||
edit: 编辑于
|
||||
commented: 评论
|
||||
Views: 阅读次数
|
||||
@@ -1695,7 +1698,7 @@ ui:
|
||||
admin_name:
|
||||
label: 名字
|
||||
msg: 名字不能为空。
|
||||
character: '只能由 "a-z"、"A-Z"、"0-9"、" - . _" 组成'
|
||||
character: '只能由 "a-z"、"0-9"、" - . _" 组成'
|
||||
msg_max_length: 名称长度必须在 2 至 30 个字符之间。
|
||||
admin_password:
|
||||
label: 密码
|
||||
|
||||
+1
-1
@@ -1292,7 +1292,7 @@ ui:
|
||||
caption: 用戶之間可以通過 "@用戶名" 進行交互。
|
||||
msg: 用戶名不能為空
|
||||
msg_range: Username must be 2-30 characters in length.
|
||||
character: '必須由 "a-z", "0-9", " - . _" 組成'
|
||||
character: '必須由 "a-z", "0-9", "- . _" 組成'
|
||||
avatar:
|
||||
label: Profile image
|
||||
gravatar: 頭像
|
||||
|
||||
@@ -25,9 +25,9 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/apache/answer/internal/base/data"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/internal/base/server"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/cli"
|
||||
"github.com/apache/answer/internal/router"
|
||||
"github.com/apache/answer/internal/service/service_config"
|
||||
"github.com/apache/answer/pkg/writer"
|
||||
@@ -98,7 +98,7 @@ func (c *AllConfig) SetEnvironmentOverrides() {
|
||||
// ReadConfig read config
|
||||
func ReadConfig(configFilePath string) (c *AllConfig, err error) {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(cli.ConfigFileDir, cli.DefaultConfigFileName)
|
||||
configFilePath = filepath.Join(path.ConfigFileDir, path.DefaultConfigFileName)
|
||||
}
|
||||
c = &AllConfig{}
|
||||
config, err := viper.NewWithPath(configFilePath)
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package path
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
DefaultReservedUsernamesConfigFileName = "reserved-usernames.json"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFileDir = "/conf/"
|
||||
UploadFilePath = "/uploads/"
|
||||
I18nPath = "/i18n/"
|
||||
CacheDir = "/cache/"
|
||||
formatAllPathOnce sync.Once
|
||||
)
|
||||
|
||||
func FormatAllPath(dataDirPath string) {
|
||||
formatAllPathOnce.Do(func() {
|
||||
ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)
|
||||
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
|
||||
I18nPath = filepath.Join(dataDirPath, I18nPath)
|
||||
CacheDir = filepath.Join(dataDirPath, CacheDir)
|
||||
})
|
||||
}
|
||||
|
||||
// GetConfigFilePath get config file path
|
||||
func GetConfigFilePath() string {
|
||||
return filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||
}
|
||||
@@ -47,6 +47,7 @@ const (
|
||||
QuestionAlreadyDeleted = "error.question.already_deleted"
|
||||
QuestionUnderReview = "error.question.under_review"
|
||||
QuestionContentCannotEmpty = "error.question.content_cannot_empty"
|
||||
QuestionContentLessThanMinimum = "error.question.content_less_than_minumum"
|
||||
AnswerNotFound = "error.answer.not_found"
|
||||
AnswerCannotDeleted = "error.answer.cannot_deleted"
|
||||
AnswerCannotUpdate = "error.answer.cannot_update"
|
||||
@@ -77,6 +78,7 @@ const (
|
||||
TagCannotUpdate = "error.tag.cannot_update"
|
||||
TagIsUsedCannotDelete = "error.tag.is_used_cannot_delete"
|
||||
TagAlreadyExist = "error.tag.already_exist"
|
||||
TagMinCount = "error.tag.minimum_count"
|
||||
RankFailToMeetTheCondition = "error.rank.fail_to_meet_the_condition"
|
||||
VoteRankFailToMeetTheCondition = "error.rank.vote_fail_to_meet_the_condition"
|
||||
NoEnoughRankToOperate = "error.rank.no_enough_rank_to_operate"
|
||||
|
||||
@@ -484,6 +484,7 @@ func copyDirEntries(sourceFs fs.FS, sourceDir, targetDir string, ignoreDir ...st
|
||||
|
||||
// Construct the absolute path for the source file/directory
|
||||
srcPath := filepath.Join(sourceDir, path)
|
||||
srcPath = filepath.ToSlash(srcPath)
|
||||
|
||||
// Construct the absolute path for the destination file/directory
|
||||
dstPath := filepath.Join(targetDir, path)
|
||||
|
||||
+8
-36
@@ -23,45 +23,17 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/apache/answer/configs"
|
||||
"github.com/apache/answer/i18n"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/pkg/dir"
|
||||
"github.com/apache/answer/pkg/writer"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultConfigFileName = "config.yaml"
|
||||
DefaultCacheFileName = "cache.db"
|
||||
DefaultReservedUsernamesConfigFileName = "reserved-usernames.json"
|
||||
)
|
||||
|
||||
var (
|
||||
ConfigFileDir = "/conf/"
|
||||
UploadFilePath = "/uploads/"
|
||||
I18nPath = "/i18n/"
|
||||
CacheDir = "/cache/"
|
||||
formatAllPathONCE sync.Once
|
||||
)
|
||||
|
||||
// GetConfigFilePath get config file path
|
||||
func GetConfigFilePath() string {
|
||||
return filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||
}
|
||||
|
||||
func FormatAllPath(dataDirPath string) {
|
||||
formatAllPathONCE.Do(func() {
|
||||
ConfigFileDir = filepath.Join(dataDirPath, ConfigFileDir)
|
||||
UploadFilePath = filepath.Join(dataDirPath, UploadFilePath)
|
||||
I18nPath = filepath.Join(dataDirPath, I18nPath)
|
||||
CacheDir = filepath.Join(dataDirPath, CacheDir)
|
||||
})
|
||||
}
|
||||
|
||||
// InstallAllInitialEnvironment install all initial environment
|
||||
func InstallAllInitialEnvironment(dataDirPath string) {
|
||||
FormatAllPath(dataDirPath)
|
||||
path.FormatAllPath(dataDirPath)
|
||||
installUploadDir()
|
||||
InstallI18nBundle(false)
|
||||
fmt.Println("install all initial environment done")
|
||||
@@ -69,7 +41,7 @@ func InstallAllInitialEnvironment(dataDirPath string) {
|
||||
|
||||
func InstallConfigFile(configFilePath string) error {
|
||||
if len(configFilePath) == 0 {
|
||||
configFilePath = filepath.Join(ConfigFileDir, DefaultConfigFileName)
|
||||
configFilePath = filepath.Join(path.ConfigFileDir, path.DefaultConfigFileName)
|
||||
}
|
||||
fmt.Println("[config-file] try to create at ", configFilePath)
|
||||
|
||||
@@ -79,7 +51,7 @@ func InstallConfigFile(configFilePath string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := dir.CreateDirIfNotExist(ConfigFileDir); err != nil {
|
||||
if err := dir.CreateDirIfNotExist(path.ConfigFileDir); err != nil {
|
||||
fmt.Printf("[config-file] create directory fail %s\n", err.Error())
|
||||
return fmt.Errorf("create directory fail %s", err.Error())
|
||||
}
|
||||
@@ -95,10 +67,10 @@ func InstallConfigFile(configFilePath string) error {
|
||||
|
||||
func installUploadDir() {
|
||||
fmt.Println("[upload-dir] try to install...")
|
||||
if err := dir.CreateDirIfNotExist(UploadFilePath); err != nil {
|
||||
if err := dir.CreateDirIfNotExist(path.UploadFilePath); err != nil {
|
||||
fmt.Printf("[upload-dir] install fail %s\n", err.Error())
|
||||
} else {
|
||||
fmt.Printf("[upload-dir] install success, upload directory is %s\n", UploadFilePath)
|
||||
fmt.Printf("[upload-dir] install success, upload directory is %s\n", path.UploadFilePath)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -108,7 +80,7 @@ func InstallI18nBundle(replace bool) {
|
||||
if len(os.Getenv("SKIP_REPLACE_I18N")) > 0 {
|
||||
replace = false
|
||||
}
|
||||
if err := dir.CreateDirIfNotExist(I18nPath); err != nil {
|
||||
if err := dir.CreateDirIfNotExist(path.I18nPath); err != nil {
|
||||
fmt.Println(err.Error())
|
||||
return
|
||||
}
|
||||
@@ -120,7 +92,7 @@ func InstallI18nBundle(replace bool) {
|
||||
}
|
||||
fmt.Printf("[i18n] find i18n bundle %d\n", len(i18nList))
|
||||
for _, item := range i18nList {
|
||||
path := filepath.Join(I18nPath, item.Name())
|
||||
path := filepath.Join(path.I18nPath, item.Name())
|
||||
content, err := i18n.I18n.ReadFile(item.Name())
|
||||
if err != nil {
|
||||
continue
|
||||
|
||||
@@ -23,6 +23,7 @@ import (
|
||||
"fmt"
|
||||
|
||||
"github.com/apache/answer/internal/base/data"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/pkg/dir"
|
||||
)
|
||||
@@ -32,7 +33,7 @@ func CheckConfigFile(configPath string) bool {
|
||||
}
|
||||
|
||||
func CheckUploadDir() bool {
|
||||
return dir.CheckDirExist(UploadFilePath)
|
||||
return dir.CheckDirExist(path.UploadFilePath)
|
||||
}
|
||||
|
||||
// CheckDBConnection check database whether the connection is normal
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package cli
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
|
||||
"github.com/apache/answer/internal/base/conf"
|
||||
"github.com/apache/answer/internal/base/data"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/internal/repo/auth"
|
||||
"github.com/apache/answer/internal/repo/user"
|
||||
authService "github.com/apache/answer/internal/service/auth"
|
||||
"github.com/apache/answer/pkg/checker"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"golang.org/x/term"
|
||||
_ "modernc.org/sqlite"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
const (
|
||||
charsetLower = "abcdefghijklmnopqrstuvwxyz"
|
||||
charsetUpper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
charsetDigits = "0123456789"
|
||||
charsetSpecial = "!@#$%^&*~?_-"
|
||||
maxRetries = 10
|
||||
defaultRandomPasswordLength = 12
|
||||
)
|
||||
|
||||
var charset = []string{
|
||||
charsetLower,
|
||||
charsetUpper,
|
||||
charsetDigits,
|
||||
charsetSpecial,
|
||||
}
|
||||
|
||||
type ResetPasswordOptions struct {
|
||||
Email string
|
||||
Password string
|
||||
}
|
||||
|
||||
func ResetPassword(ctx context.Context, dataDirPath string, opts *ResetPasswordOptions) error {
|
||||
path.FormatAllPath(dataDirPath)
|
||||
|
||||
config, err := conf.ReadConfig(path.GetConfigFilePath())
|
||||
if err != nil {
|
||||
return fmt.Errorf("read config file failed: %w", err)
|
||||
}
|
||||
|
||||
db, err := initDatabase(config.Data.Database.Driver, config.Data.Database.Connection)
|
||||
if err != nil {
|
||||
return fmt.Errorf("connect database failed: %w", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
cache, cacheCleanup, err := data.NewCache(config.Data.Cache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize cache failed: %w", err)
|
||||
}
|
||||
defer cacheCleanup()
|
||||
|
||||
dataData, dataCleanup, err := data.NewData(db, cache)
|
||||
if err != nil {
|
||||
return fmt.Errorf("initialize data layer failed: %w", err)
|
||||
}
|
||||
defer dataCleanup()
|
||||
|
||||
userRepo := user.NewUserRepo(dataData)
|
||||
authRepo := auth.NewAuthRepo(dataData)
|
||||
authSvc := authService.NewAuthService(authRepo)
|
||||
|
||||
email := strings.TrimSpace(opts.Email)
|
||||
if email == "" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Please input user email: ")
|
||||
emailInput, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return fmt.Errorf("read email input failed: %w", err)
|
||||
}
|
||||
email = strings.TrimSpace(emailInput)
|
||||
}
|
||||
|
||||
userInfo, exist, err := userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
return fmt.Errorf("query user failed: %w", err)
|
||||
}
|
||||
if !exist {
|
||||
return fmt.Errorf("user not found: %s", email)
|
||||
}
|
||||
|
||||
fmt.Printf("You are going to reset password for user: %s\n", email)
|
||||
|
||||
password := strings.TrimSpace(opts.Password)
|
||||
|
||||
if password != "" {
|
||||
printWarning("Passing password via command line may be recorded in shell history")
|
||||
if err := checker.CheckPassword(password); err != nil {
|
||||
return fmt.Errorf("password validation failed: %w", err)
|
||||
}
|
||||
} else {
|
||||
password, err = promptForPassword()
|
||||
if err != nil {
|
||||
return fmt.Errorf("password input failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
if !confirmAction(fmt.Sprintf("This will reset password for user '[%s]%s'. Continue?", userInfo.DisplayName, email)) {
|
||||
fmt.Println("Operation cancelled")
|
||||
return nil
|
||||
}
|
||||
|
||||
hashPwd, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt password failed: %w", err)
|
||||
}
|
||||
|
||||
if err = userRepo.UpdatePass(ctx, userInfo.ID, string(hashPwd)); err != nil {
|
||||
return fmt.Errorf("update password failed: %w", err)
|
||||
}
|
||||
|
||||
authSvc.RemoveUserAllTokens(ctx, userInfo.ID)
|
||||
|
||||
fmt.Printf("Password has been successfully updated for user: %s\n", email)
|
||||
fmt.Println("All login sessions have been cleared")
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// promptForPassword prompts for a password
|
||||
func promptForPassword() (string, error) {
|
||||
for {
|
||||
input, err := getPasswordInput("Please input new password (empty to generate random password): ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if input == "" {
|
||||
password, err := generateRandomPasswordWithRetry()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("generate random password failed: %w", err)
|
||||
}
|
||||
fmt.Printf("Generated random password: %s\n", password)
|
||||
fmt.Println("Please save this password in a secure location")
|
||||
return password, nil
|
||||
}
|
||||
|
||||
if err := checker.CheckPassword(input); err != nil {
|
||||
fmt.Printf("Password validation failed: %v\n", err)
|
||||
fmt.Println("Please try again")
|
||||
continue
|
||||
}
|
||||
|
||||
confirmPwd, err := getPasswordInput("Please confirm new password: ")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if input != confirmPwd {
|
||||
fmt.Println("Passwords do not match, please try again")
|
||||
continue
|
||||
}
|
||||
|
||||
return input, nil
|
||||
}
|
||||
}
|
||||
|
||||
func generateRandomPasswordWithRetry() (string, error) {
|
||||
var password string
|
||||
var err error
|
||||
|
||||
for range maxRetries {
|
||||
password, err = generateRandomPassword(defaultRandomPasswordLength)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if err := checker.CheckPassword(password); err == nil {
|
||||
return password, nil
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return "", fmt.Errorf("failed to generate valid password after %d retries", maxRetries)
|
||||
}
|
||||
|
||||
func getPasswordInput(prompt string) (string, error) {
|
||||
fmt.Print(prompt)
|
||||
password, err := term.ReadPassword(int(os.Stdin.Fd()))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
fmt.Println()
|
||||
return string(password), nil
|
||||
}
|
||||
|
||||
func generateRandomPassword(length int) (string, error) {
|
||||
if length < len(charset) {
|
||||
return "", fmt.Errorf("password length must be at least %d", len(charset))
|
||||
}
|
||||
|
||||
bytes := make([]byte, length)
|
||||
for i, charsetItem := range charset {
|
||||
charIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(charsetItem))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bytes[i] = charsetItem[charIndex.Int64()]
|
||||
}
|
||||
|
||||
fullCharset := strings.Join(charset, "")
|
||||
for i := len(charset); i < length; i++ {
|
||||
charIndex, err := rand.Int(rand.Reader, big.NewInt(int64(len(fullCharset))))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bytes[i] = fullCharset[charIndex.Int64()]
|
||||
}
|
||||
|
||||
for i := len(bytes) - 1; i > 0; i-- {
|
||||
j, err := rand.Int(rand.Reader, big.NewInt(int64(i+1)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
bytes[i], bytes[j.Int64()] = bytes[j.Int64()], bytes[i]
|
||||
}
|
||||
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func initDatabase(driver, connection string) (*xorm.Engine, error) {
|
||||
dataConf := &data.Database{Driver: driver, Connection: connection}
|
||||
if !CheckDBConnection(dataConf) {
|
||||
return nil, fmt.Errorf("database connection check failed")
|
||||
}
|
||||
|
||||
engine, err := data.NewDB(false, dataConf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
func printWarning(msg string) {
|
||||
if runtime.GOOS == "windows" {
|
||||
fmt.Printf("[WARNING] %s\n", msg)
|
||||
} else {
|
||||
fmt.Printf("\033[31m[WARNING] %s\033[0m\n", msg)
|
||||
}
|
||||
}
|
||||
|
||||
func confirmAction(prompt string) bool {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Printf("%s [y/N]: ", prompt)
|
||||
response, err := reader.ReadString('\n')
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
response = strings.ToLower(strings.TrimSpace(response))
|
||||
return response == "y" || response == "yes"
|
||||
}
|
||||
@@ -20,6 +20,8 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/middleware"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
@@ -34,7 +36,6 @@ import (
|
||||
"github.com/apache/answer/pkg/uid"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// CommentController comment controller
|
||||
@@ -120,6 +121,9 @@ func (cc *CommentController) AddComment(ctx *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
req.UserAgent = ctx.GetHeader("User-Agent")
|
||||
req.IP = ctx.ClientIP()
|
||||
|
||||
resp, err := cc.commentService.AddComment(ctx, req)
|
||||
if !isAdmin || !linkUrlLimitUser {
|
||||
cc.actionService.ActionRecordAdd(ctx, entity.CaptchaActionComment, req.UserID)
|
||||
|
||||
@@ -53,4 +53,5 @@ var ProviderSetController = wire.NewSet(
|
||||
NewEmbedController,
|
||||
NewBadgeController,
|
||||
NewRenderController,
|
||||
NewSidebarController,
|
||||
)
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/plugin"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SidebarController is the controller for the sidebar plugin.
|
||||
type SidebarController struct{}
|
||||
|
||||
// NewSidebarController creates a new instance of SidebarController.
|
||||
func NewSidebarController() *SidebarController {
|
||||
return &SidebarController{}
|
||||
}
|
||||
|
||||
// GetSidebarConfig retrieves the sidebar configuration from the registered sidebar plugins.
|
||||
func (uc *SidebarController) GetSidebarConfig(ctx *gin.Context) {
|
||||
resp := &plugin.SidebarConfig{}
|
||||
_ = plugin.CallSidebar(func(fn plugin.Sidebar) error {
|
||||
cfg, err := fn.GetSidebarConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resp = cfg
|
||||
return nil
|
||||
})
|
||||
handler.HandleResponse(ctx, nil, resp)
|
||||
}
|
||||
@@ -87,3 +87,8 @@ func (c *Comment) SetReplyCommentID(str string) {
|
||||
c.ReplyCommentID = sql.NullInt64{Valid: false}
|
||||
}
|
||||
}
|
||||
|
||||
// GetMentionUsernameList get mention username list
|
||||
func (c *Comment) GetMentionUsernameList() []string {
|
||||
return converter.GetMentionUsernameList(c.OriginalText)
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ import (
|
||||
"github.com/apache/answer/internal/base/conf"
|
||||
"github.com/apache/answer/internal/base/data"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/cli"
|
||||
@@ -62,7 +63,7 @@ func LangOptions(ctx *gin.Context) {
|
||||
// @Success 200 {object} handler.RespBody{}
|
||||
// @Router /installation/language/config [get]
|
||||
func GetLangMapping(ctx *gin.Context) {
|
||||
t, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath})
|
||||
t, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath})
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
return
|
||||
@@ -186,9 +187,9 @@ func InitEnvironment(ctx *gin.Context) {
|
||||
}
|
||||
c.Data.Database.Driver = req.DbType
|
||||
c.Data.Database.Connection = req.GetConnection()
|
||||
c.Data.Cache.FilePath = filepath.Join(cli.CacheDir, cli.DefaultCacheFileName)
|
||||
c.I18n.BundleDir = cli.I18nPath
|
||||
c.ServiceConfig.UploadPath = cli.UploadFilePath
|
||||
c.Data.Cache.FilePath = filepath.Join(path.CacheDir, path.DefaultCacheFileName)
|
||||
c.I18n.BundleDir = path.I18nPath
|
||||
c.ServiceConfig.UploadPath = path.UploadFilePath
|
||||
|
||||
if err := conf.RewriteConfig(confPath, c); err != nil {
|
||||
log.Errorf("rewrite config failed %s", err)
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/cli"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -35,7 +35,7 @@ var (
|
||||
func Run(configPath string) {
|
||||
confPath = configPath
|
||||
// initialize translator for return internationalization error when installing.
|
||||
_, err := translator.NewTranslator(&translator.I18n{BundleDir: cli.I18nPath})
|
||||
_, err := translator.NewTranslator(&translator.I18n{BundleDir: path.I18nPath})
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
@@ -289,7 +289,9 @@ func (m *Mentor) initSiteInfoPrivilegeRank() {
|
||||
|
||||
func (m *Mentor) initSiteInfoWrite() {
|
||||
writeData := map[string]interface{}{
|
||||
"min_content": 6,
|
||||
"restrict_answer": true,
|
||||
"min_tags": 1,
|
||||
"required_tag": false,
|
||||
"recommend_tags": []string{},
|
||||
"reserved_tags": []string{},
|
||||
|
||||
@@ -103,6 +103,7 @@ var migrations = []Migration{
|
||||
NewMigration("v1.4.5", "add file record", addFileRecord, true),
|
||||
NewMigration("v1.5.1", "add plugin kv storage", addPluginKVStorage, true),
|
||||
NewMigration("v1.6.0", "move user config to interface", moveUserConfigToInterface, true),
|
||||
NewMigration("v1.7.0", "add optional tags", addOptionalTags, true),
|
||||
}
|
||||
|
||||
func GetMigrations() []Migration {
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
package migrations
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
func addOptionalTags(ctx context.Context, x *xorm.Engine) error {
|
||||
writeSiteInfo := &entity.SiteInfo{
|
||||
Type: constant.SiteTypeWrite,
|
||||
}
|
||||
exist, err := x.Context(ctx).Get(writeSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get config failed: %w", err)
|
||||
}
|
||||
if exist {
|
||||
type OldSiteWriteReq struct {
|
||||
MinimumContent int `json:"min_content"`
|
||||
RestrictAnswer bool `json:"restrict_answer"`
|
||||
MinimumTags int `json:"min_tags"`
|
||||
RequiredTag bool `json:"required_tag"`
|
||||
RecommendTags []*schema.SiteWriteTag `json:"recommend_tags"`
|
||||
ReservedTags []*schema.SiteWriteTag `json:"reserved_tags"`
|
||||
MaxImageSize int `json:"max_image_size"`
|
||||
MaxAttachmentSize int `json:"max_attachment_size"`
|
||||
MaxImageMegapixel int `json:"max_image_megapixel"`
|
||||
AuthorizedImageExtensions []string `json:"authorized_image_extensions"`
|
||||
AuthorizedAttachmentExtensions []string `json:"authorized_attachment_extensions"`
|
||||
}
|
||||
content := &OldSiteWriteReq{}
|
||||
_ = json.Unmarshal([]byte(writeSiteInfo.Content), content)
|
||||
content.MinimumTags = 1
|
||||
content.MinimumContent = 6
|
||||
data, _ := json.Marshal(content)
|
||||
writeSiteInfo.Content = string(data)
|
||||
_, err = x.Context(ctx).ID(writeSiteInfo.ID).Cols("content").Update(writeSiteInfo)
|
||||
if err != nil {
|
||||
return fmt.Errorf("update site info failed: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -92,6 +92,17 @@ func (cr *commentRepo) UpdateCommentContent(
|
||||
return
|
||||
}
|
||||
|
||||
// UpdateCommentStatus update comment status
|
||||
func (cr *commentRepo) UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error) {
|
||||
_, err = cr.data.DB.Context(ctx).ID(commentID).Update(&entity.Comment{
|
||||
Status: status,
|
||||
})
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// GetComment get comment one
|
||||
func (cr *commentRepo) GetComment(ctx context.Context, commentID string) (
|
||||
comment *entity.Comment, exist bool, err error) {
|
||||
|
||||
@@ -22,6 +22,7 @@ package repo_test
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/repo/auth"
|
||||
@@ -53,7 +54,7 @@ func Test_userAdminRepo_UpdateUserStatus(t *testing.T) {
|
||||
assert.Equal(t, entity.UserStatusAvailable, got.Status)
|
||||
|
||||
err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusSuspended, entity.EmailStatusAvailable,
|
||||
"admin@admin.com")
|
||||
"admin@admin.com", time.Now().Add(time.Minute*5))
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1")
|
||||
@@ -62,7 +63,7 @@ func Test_userAdminRepo_UpdateUserStatus(t *testing.T) {
|
||||
assert.Equal(t, entity.UserStatusSuspended, got.Status)
|
||||
|
||||
err = userAdminRepo.UpdateUserStatus(context.TODO(), "1", entity.UserStatusAvailable, entity.EmailStatusAvailable,
|
||||
"admin@admin.com")
|
||||
"admin@admin.com", time.Time{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
got, exist, err = userAdminRepo.GetUserInfo(context.TODO(), "1")
|
||||
|
||||
@@ -30,6 +30,7 @@ type PluginAPIRouter struct {
|
||||
captchaController *controller.CaptchaController
|
||||
embedController *controller.EmbedController
|
||||
renderController *controller.RenderController
|
||||
sidebarController *controller.SidebarController
|
||||
}
|
||||
|
||||
func NewPluginAPIRouter(
|
||||
@@ -38,6 +39,7 @@ func NewPluginAPIRouter(
|
||||
captchaController *controller.CaptchaController,
|
||||
embedController *controller.EmbedController,
|
||||
renderController *controller.RenderController,
|
||||
sidebarController *controller.SidebarController,
|
||||
) *PluginAPIRouter {
|
||||
return &PluginAPIRouter{
|
||||
connectorController: connectorController,
|
||||
@@ -45,6 +47,7 @@ func NewPluginAPIRouter(
|
||||
captchaController: captchaController,
|
||||
embedController: embedController,
|
||||
renderController: renderController,
|
||||
sidebarController: sidebarController,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +71,9 @@ func (pr *PluginAPIRouter) RegisterUnAuthConnectorRouter(r *gin.RouterGroup) {
|
||||
r.GET("/captcha/config", pr.captchaController.GetCaptchaConfig)
|
||||
r.GET("/embed/config", pr.embedController.GetEmbedConfig)
|
||||
r.GET("/render/config", pr.renderController.GetRenderConfig)
|
||||
|
||||
// sidebar plugin
|
||||
r.GET("/sidebar/config", pr.sidebarController.GetSidebarConfig)
|
||||
}
|
||||
|
||||
func (pr *PluginAPIRouter) RegisterAuthUserConnectorRouter(r *gin.RouterGroup) {
|
||||
|
||||
@@ -51,6 +51,9 @@ type AddCommentReq struct {
|
||||
CanEdit bool `json:"-"`
|
||||
// whether user can delete it
|
||||
CanDelete bool `json:"-"`
|
||||
|
||||
IP string `json:"-"`
|
||||
UserAgent string `json:"-"`
|
||||
}
|
||||
|
||||
func (req *AddCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
|
||||
@@ -79,11 +79,11 @@ type QuestionAdd struct {
|
||||
// question title
|
||||
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
|
||||
// content
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
Content string `validate:"gte=0,lte=65535" json:"content"`
|
||||
// html
|
||||
HTML string `json:"-"`
|
||||
// tags
|
||||
Tags []*TagItem `validate:"required,dive" json:"tags"`
|
||||
Tags []*TagItem `validate:"dive" json:"tags"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
QuestionPermission
|
||||
@@ -100,12 +100,6 @@ func (req *QuestionAdd) Check() (errFields []*validator.FormErrorField, err erro
|
||||
tag.ParsedText = converter.Markdown2HTML(tag.OriginalText)
|
||||
}
|
||||
}
|
||||
if req.HTML == "" {
|
||||
return append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "content",
|
||||
ErrorMsg: reason.QuestionContentCannotEmpty,
|
||||
}), errors.BadRequest(reason.QuestionContentCannotEmpty)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -113,13 +107,13 @@ type QuestionAddByAnswer struct {
|
||||
// question title
|
||||
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
|
||||
// content
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
Content string `validate:"gte=0,lte=65535" json:"content"`
|
||||
// html
|
||||
HTML string `json:"-"`
|
||||
AnswerContent string `validate:"required,notblank,gte=6,lte=65535" json:"answer_content"`
|
||||
AnswerHTML string `json:"-"`
|
||||
// tags
|
||||
Tags []*TagItem `validate:"required,dive" json:"tags"`
|
||||
Tags []*TagItem `validate:"dive" json:"tags"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
|
||||
@@ -138,19 +132,11 @@ func (req *QuestionAddByAnswer) Check() (errFields []*validator.FormErrorField,
|
||||
tag.ParsedText = converter.Markdown2HTML(tag.OriginalText)
|
||||
}
|
||||
}
|
||||
if req.HTML == "" {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "content",
|
||||
ErrorMsg: reason.QuestionContentCannotEmpty,
|
||||
})
|
||||
}
|
||||
if req.AnswerHTML == "" {
|
||||
errFields = append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "answer_content",
|
||||
ErrorMsg: reason.AnswerContentCannotEmpty,
|
||||
})
|
||||
}
|
||||
if req.HTML == "" || req.AnswerHTML == "" {
|
||||
return errFields, errors.BadRequest(reason.QuestionContentCannotEmpty)
|
||||
}
|
||||
return nil, nil
|
||||
@@ -195,12 +181,12 @@ type QuestionUpdate struct {
|
||||
// question title
|
||||
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
|
||||
// content
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
Content string `validate:"gte=0,lte=65535" json:"content"`
|
||||
// html
|
||||
HTML string `json:"-"`
|
||||
InviteUser []string `validate:"omitempty" json:"invite_user"`
|
||||
// tags
|
||||
Tags []*TagItem `validate:"required,dive" json:"tags"`
|
||||
Tags []*TagItem `validate:"dive" json:"tags"`
|
||||
// edit summary
|
||||
EditSummary string `validate:"omitempty" json:"edit_summary"`
|
||||
// user id
|
||||
@@ -227,12 +213,6 @@ type QuestionUpdateInviteUser struct {
|
||||
|
||||
func (req *QuestionUpdate) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
req.HTML = converter.Markdown2HTML(req.Content)
|
||||
if req.HTML == "" {
|
||||
return append(errFields, &validator.FormErrorField{
|
||||
ErrorField: "content",
|
||||
ErrorMsg: reason.QuestionContentCannotEmpty,
|
||||
}), errors.BadRequest(reason.QuestionContentCannotEmpty)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,9 @@ type SiteBrandingReq struct {
|
||||
|
||||
// SiteWriteReq site write request
|
||||
type SiteWriteReq struct {
|
||||
MinimumContent int `validate:"omitempty,gte=0,lte=65535" json:"min_content"`
|
||||
RestrictAnswer bool `validate:"omitempty" json:"restrict_answer"`
|
||||
MinimumTags int `validate:"omitempty,gte=0,lte=5" json:"min_tags"`
|
||||
RequiredTag bool `validate:"omitempty" json:"required_tag"`
|
||||
RecommendTags []*SiteWriteTag `validate:"omitempty,dive" json:"recommend_tags"`
|
||||
ReservedTags []*SiteWriteTag `validate:"omitempty,dive" json:"reserved_tags"`
|
||||
|
||||
@@ -21,7 +21,10 @@ package comment
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/apache/answer/internal/service/event_queue"
|
||||
"github.com/apache/answer/internal/service/review"
|
||||
|
||||
"time"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
@@ -50,6 +53,7 @@ type CommentRepo interface {
|
||||
AddComment(ctx context.Context, comment *entity.Comment) (err error)
|
||||
RemoveComment(ctx context.Context, commentID string) (err error)
|
||||
UpdateCommentContent(ctx context.Context, commentID string, original string, parsedText string) (err error)
|
||||
UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error)
|
||||
GetComment(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
|
||||
GetCommentPage(ctx context.Context, commentQuery *CommentQuery) (
|
||||
comments []*entity.Comment, total int64, err error)
|
||||
@@ -88,6 +92,7 @@ type CommentService struct {
|
||||
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
|
||||
activityQueueService activity_queue.ActivityQueueService
|
||||
eventQueueService event_queue.EventQueueService
|
||||
reviewService *review.ReviewService
|
||||
}
|
||||
|
||||
// NewCommentService new comment service
|
||||
@@ -103,6 +108,7 @@ func NewCommentService(
|
||||
externalNotificationQueueService notice_queue.ExternalNotificationQueueService,
|
||||
activityQueueService activity_queue.ActivityQueueService,
|
||||
eventQueueService event_queue.EventQueueService,
|
||||
reviewService *review.ReviewService,
|
||||
) *CommentService {
|
||||
return &CommentService{
|
||||
commentRepo: commentRepo,
|
||||
@@ -116,6 +122,7 @@ func NewCommentService(
|
||||
externalNotificationQueueService: externalNotificationQueueService,
|
||||
activityQueueService: activityQueueService,
|
||||
eventQueueService: eventQueueService,
|
||||
reviewService: reviewService,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,14 +167,21 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
||||
return nil, err
|
||||
}
|
||||
|
||||
comment.Status = cs.reviewService.AddCommentReview(ctx, comment, req.IP, req.UserAgent)
|
||||
if err := cs.commentRepo.UpdateCommentStatus(ctx, comment.ID, comment.Status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
resp = &schema.GetCommentResp{}
|
||||
resp.SetFromComment(comment)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
|
||||
time.Now(), req.CanEdit, req.CanDelete)
|
||||
|
||||
commentResp, err := cs.addCommentNotification(ctx, req, resp, comment, objInfo)
|
||||
if err != nil {
|
||||
return commentResp, err
|
||||
if comment.Status == entity.CommentStatusAvailable {
|
||||
commentResp, err := cs.addCommentNotification(ctx, req, resp, comment, objInfo)
|
||||
if err != nil {
|
||||
return commentResp, err
|
||||
}
|
||||
}
|
||||
|
||||
// get user info
|
||||
|
||||
@@ -34,6 +34,7 @@ type CommentCommonRepo interface {
|
||||
GetCommentWithoutStatus(ctx context.Context, commentID string) (comment *entity.Comment, exist bool, err error)
|
||||
GetCommentCount(ctx context.Context) (count int64, err error)
|
||||
RemoveAllUserComment(ctx context.Context, userID string) (err error)
|
||||
UpdateCommentStatus(ctx context.Context, commentID string, status int) (err error)
|
||||
}
|
||||
|
||||
// CommentCommonService user service
|
||||
|
||||
@@ -229,13 +229,30 @@ func (qs *QuestionService) AddQuestionCheckTags(ctx context.Context, Tags []*ent
|
||||
return []string{}, nil
|
||||
}
|
||||
func (qs *QuestionService) CheckAddQuestion(ctx context.Context, req *schema.QuestionAdd) (errorlist any, err error) {
|
||||
if len(req.Tags) == 0 {
|
||||
minimumTags, err := qs.tagCommon.GetMinimumTags(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Tags) < minimumTags {
|
||||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
err = errors.BadRequest(reason.TagMinCount)
|
||||
return errorlist, err
|
||||
}
|
||||
minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Content) < minimumContentLength {
|
||||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "content",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),
|
||||
})
|
||||
err = errors.BadRequest(reason.QuestionContentLessThanMinimum)
|
||||
return errorlist, err
|
||||
}
|
||||
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
|
||||
@@ -284,13 +301,30 @@ func (qs *QuestionService) HasNewTag(ctx context.Context, tags []*schema.TagItem
|
||||
|
||||
// AddQuestion add question
|
||||
func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.QuestionAdd) (questionInfo any, err error) {
|
||||
if len(req.Tags) == 0 {
|
||||
minimumTags, err := qs.tagCommon.GetMinimumTags(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Tags) < minimumTags {
|
||||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagNotFound),
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),
|
||||
})
|
||||
err = errors.BadRequest(reason.RecommendTagEnter)
|
||||
err = errors.BadRequest(reason.TagMinCount)
|
||||
return errorlist, err
|
||||
}
|
||||
minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Content) < minimumContentLength {
|
||||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "content",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),
|
||||
})
|
||||
err = errors.BadRequest(reason.QuestionContentLessThanMinimum)
|
||||
return errorlist, err
|
||||
}
|
||||
recommendExist, err := qs.tagCommon.ExistRecommend(ctx, req.Tags)
|
||||
@@ -370,9 +404,9 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
||||
objectTagData.ObjectID = question.ID
|
||||
objectTagData.Tags = req.Tags
|
||||
objectTagData.UserID = req.UserID
|
||||
err = qs.ChangeTag(ctx, &objectTagData)
|
||||
errorlist, err := qs.ChangeTag(ctx, &objectTagData)
|
||||
if err != nil {
|
||||
return
|
||||
return errorlist, err
|
||||
}
|
||||
_ = qs.questionRepo.UpdateSearch(ctx, question.ID)
|
||||
|
||||
@@ -413,8 +447,15 @@ func (qs *QuestionService) AddQuestion(ctx context.Context, req *schema.Question
|
||||
})
|
||||
|
||||
if question.Status == entity.QuestionStatusAvailable {
|
||||
qs.externalNotificationQueueService.Send(ctx,
|
||||
schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags))
|
||||
newTags, newTagsErr := qs.tagCommon.GetTagListByNames(ctx, tagNameList)
|
||||
if newTagsErr != nil {
|
||||
log.Error("get question newTags error %v", newTagsErr)
|
||||
qs.externalNotificationQueueService.Send(ctx,
|
||||
schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, tags))
|
||||
} else {
|
||||
qs.externalNotificationQueueService.Send(ctx,
|
||||
schema.CreateNewQuestionNotificationMsg(question.ID, question.Title, question.UserID, newTags))
|
||||
}
|
||||
}
|
||||
qs.eventQueueService.Send(ctx, schema.NewEvent(constant.EventQuestionCreate, req.UserID).TID(question.ID).
|
||||
QID(question.ID, question.UserID))
|
||||
@@ -892,6 +933,20 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
||||
question.UserID = dbinfo.UserID
|
||||
question.LastEditUserID = req.UserID
|
||||
|
||||
minimumContentLength, err := qs.questioncommon.GetMinimumContentLength(ctx)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if len(req.Content) < minimumContentLength {
|
||||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "content",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.QuestionContentLessThanMinimum),
|
||||
})
|
||||
err = errors.BadRequest(reason.QuestionContentLessThanMinimum)
|
||||
return errorlist, err
|
||||
}
|
||||
|
||||
oldTags, tagerr := qs.tagCommon.GetObjectEntityTag(ctx, question.ID)
|
||||
if tagerr != nil {
|
||||
return questionInfo, tagerr
|
||||
@@ -993,9 +1048,9 @@ func (qs *QuestionService) UpdateQuestion(ctx context.Context, req *schema.Quest
|
||||
objectTagData.ObjectID = question.ID
|
||||
objectTagData.Tags = req.Tags
|
||||
objectTagData.UserID = req.UserID
|
||||
tagerr := qs.ChangeTag(ctx, &objectTagData)
|
||||
errorlist, tagerr := qs.ChangeTag(ctx, &objectTagData)
|
||||
if tagerr != nil {
|
||||
return questionInfo, tagerr
|
||||
return errorlist, tagerr
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1095,8 +1150,12 @@ func (qs *QuestionService) InviteUserInfo(ctx context.Context, questionID string
|
||||
return qs.questioncommon.InviteUserInfo(ctx, questionID)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) error {
|
||||
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData)
|
||||
func (qs *QuestionService) ChangeTag(ctx context.Context, objectTagData *schema.TagChange) (errorlist []*validator.FormErrorField, err error) {
|
||||
minimumTags, err := qs.tagCommon.GetMinimumTags(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return qs.tagCommon.ObjectChangeTag(ctx, objectTagData, minimumTags)
|
||||
}
|
||||
|
||||
func (qs *QuestionService) CheckChangeReservedTag(ctx context.Context, oldobjectTagData, objectTagData []*entity.Tag) (bool, bool, []string, []string) {
|
||||
|
||||
@@ -214,7 +214,11 @@ func (rs *RevisionService) revisionAuditQuestion(ctx context.Context, revisionit
|
||||
objectTagData := schema.TagChange{}
|
||||
objectTagData.ObjectID = question.ID
|
||||
objectTagData.Tags = objectTagTags
|
||||
saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData)
|
||||
minimumTags, err := rs.tagCommon.GetMinimumTags(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, saveerr = rs.tagCommon.ObjectChangeTag(ctx, &objectTagData, minimumTags)
|
||||
if saveerr != nil {
|
||||
return saveerr
|
||||
}
|
||||
|
||||
@@ -1,22 +1,3 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Code generated by MockGen. DO NOT EDIT.
|
||||
// Source: ./siteinfo_service.go
|
||||
//
|
||||
|
||||
@@ -279,5 +279,17 @@ func (ns *ExternalNotificationService) newPluginQuestionNotification(
|
||||
raw.QuestionUrl = display.QuestionURL(
|
||||
seoInfo.Permalink, siteInfo.SiteUrl,
|
||||
msg.NewQuestionTemplateRawData.QuestionID, msg.NewQuestionTemplateRawData.QuestionTitle)
|
||||
if len(msg.NewQuestionTemplateRawData.QuestionAuthorUserID) > 0 {
|
||||
triggerUser, exist, err := ns.userRepo.GetByUserID(ctx, msg.NewQuestionTemplateRawData.QuestionAuthorUserID)
|
||||
if err != nil {
|
||||
log.Errorf("get trigger user basic info failed: %v", err)
|
||||
return
|
||||
}
|
||||
if exist {
|
||||
raw.TriggerUserID = triggerUser.ID
|
||||
raw.TriggerUserDisplayName = triggerUser.DisplayName
|
||||
raw.TriggerUserUrl = display.UserURL(siteInfo.SiteUrl, triggerUser.Username)
|
||||
}
|
||||
}
|
||||
return raw
|
||||
}
|
||||
|
||||
@@ -899,3 +899,11 @@ func (qs *QuestionCommon) tryToGetQuestionIDFromMsg(ctx context.Context, closeMs
|
||||
questionID = uid.DeShortID(questionID)
|
||||
return questionID
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) GetMinimumContentLength(ctx context.Context) (int, error) {
|
||||
siteInfo, err := qs.siteInfoService.GetSiteWrite(ctx)
|
||||
if err != nil {
|
||||
return 6, err
|
||||
}
|
||||
return siteInfo.MinimumContent, nil
|
||||
}
|
||||
|
||||
@@ -28,6 +28,7 @@ import (
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
answercommon "github.com/apache/answer/internal/service/answer_common"
|
||||
commentcommon "github.com/apache/answer/internal/service/comment_common"
|
||||
"github.com/apache/answer/internal/service/notice_queue"
|
||||
"github.com/apache/answer/internal/service/object_info"
|
||||
questioncommon "github.com/apache/answer/internal/service/question_common"
|
||||
@@ -68,6 +69,7 @@ type ReviewService struct {
|
||||
externalNotificationQueueService notice_queue.ExternalNotificationQueueService
|
||||
notificationQueueService notice_queue.NotificationQueueService
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService
|
||||
commentCommonRepo commentcommon.CommentCommonRepo
|
||||
}
|
||||
|
||||
// NewReviewService new review service
|
||||
@@ -84,6 +86,7 @@ func NewReviewService(
|
||||
questionCommon *questioncommon.QuestionCommon,
|
||||
notificationQueueService notice_queue.NotificationQueueService,
|
||||
siteInfoService siteinfo_common.SiteInfoCommonService,
|
||||
commentCommonRepo commentcommon.CommentCommonRepo,
|
||||
) *ReviewService {
|
||||
return &ReviewService{
|
||||
reviewRepo: reviewRepo,
|
||||
@@ -98,6 +101,7 @@ func NewReviewService(
|
||||
questionCommon: questionCommon,
|
||||
notificationQueueService: notificationQueueService,
|
||||
siteInfoService: siteInfoService,
|
||||
commentCommonRepo: commentCommonRepo,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -153,6 +157,30 @@ func (cs *ReviewService) AddAnswerReview(ctx context.Context,
|
||||
return answerStatus
|
||||
}
|
||||
|
||||
// AddCommentReview add review for comment if needed
|
||||
func (cs *ReviewService) AddCommentReview(ctx context.Context,
|
||||
comment *entity.Comment, ip, ua string) (commentStatus int) {
|
||||
reviewContent := &plugin.ReviewContent{
|
||||
ObjectType: constant.CommentObjectType,
|
||||
Content: comment.ParsedText,
|
||||
IP: ip,
|
||||
UserAgent: ua,
|
||||
}
|
||||
reviewContent.Author = cs.getReviewContentAuthorInfo(ctx, comment.UserID)
|
||||
reviewStatus := cs.callPluginToReview(ctx, comment.UserID, comment.ID, reviewContent)
|
||||
switch reviewStatus {
|
||||
case plugin.ReviewStatusApproved:
|
||||
commentStatus = entity.CommentStatusAvailable
|
||||
case plugin.ReviewStatusNeedReview:
|
||||
commentStatus = entity.CommentStatusPending
|
||||
case plugin.ReviewStatusDeleteDirectly:
|
||||
commentStatus = entity.CommentStatusDeleted
|
||||
default:
|
||||
commentStatus = entity.CommentStatusAvailable
|
||||
}
|
||||
return commentStatus
|
||||
}
|
||||
|
||||
// get review content author info
|
||||
func (cs *ReviewService) getReviewContentAuthorInfo(ctx context.Context, userID string) (author plugin.ReviewContentAuthor) {
|
||||
user, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, userID)
|
||||
@@ -314,6 +342,32 @@ func (cs *ReviewService) updateObjectStatus(ctx context.Context, review *entity.
|
||||
log.Errorf("update user answer count failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
case constant.CommentObjectType:
|
||||
commentInfo, exist, err := cs.commentCommonRepo.GetCommentWithoutStatus(ctx, review.ObjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.ObjectNotFound)
|
||||
}
|
||||
if isApprove {
|
||||
commentInfo.Status = entity.CommentStatusAvailable
|
||||
} else {
|
||||
commentInfo.Status = entity.CommentStatusDeleted
|
||||
}
|
||||
if err := cs.commentCommonRepo.UpdateCommentStatus(ctx, commentInfo.ID, commentInfo.Status); err != nil {
|
||||
return err
|
||||
}
|
||||
_, exist, err = cs.questionRepo.GetQuestion(ctx, commentInfo.QuestionID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.ObjectNotFound)
|
||||
}
|
||||
if isApprove {
|
||||
cs.notificationCommentOnTheQuestion(ctx, commentInfo)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -364,6 +418,222 @@ func (cs *ReviewService) notificationAnswerTheQuestion(ctx context.Context,
|
||||
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
|
||||
}
|
||||
|
||||
func (cs *ReviewService) notificationCommentOnTheQuestion(ctx context.Context, comment *entity.Comment) {
|
||||
objInfo, err := cs.objectInfoService.GetInfo(ctx, comment.ObjectID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if objInfo.IsDeleted() {
|
||||
log.Error("object already deleted")
|
||||
return
|
||||
}
|
||||
objInfo.ObjectID = uid.DeShortID(objInfo.ObjectID)
|
||||
objInfo.QuestionID = uid.DeShortID(objInfo.QuestionID)
|
||||
objInfo.AnswerID = uid.DeShortID(objInfo.AnswerID)
|
||||
|
||||
// The priority of the notification
|
||||
// 1. reply to user
|
||||
// 2. comment mention to user
|
||||
// 3. answer or question was commented
|
||||
alreadyNotifiedUserID := make(map[string]bool)
|
||||
|
||||
// get reply user info
|
||||
replyUserID := comment.GetReplyUserID()
|
||||
if len(replyUserID) > 0 && replyUserID != comment.UserID {
|
||||
replyUser, _, err := cs.userCommon.GetUserBasicInfoByID(ctx, replyUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, comment.UserID,
|
||||
objInfo.QuestionID, objInfo.Title, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
|
||||
alreadyNotifiedUserID[replyUser.ID] = true
|
||||
return
|
||||
}
|
||||
|
||||
mentionUsernameList := comment.GetMentionUsernameList()
|
||||
if len(mentionUsernameList) > 0 {
|
||||
alreadyNotifiedUserIDs := cs.notificationMention(
|
||||
ctx, mentionUsernameList, comment.ID, comment.UserID, alreadyNotifiedUserID)
|
||||
for _, userID := range alreadyNotifiedUserIDs {
|
||||
alreadyNotifiedUserID[userID] = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
|
||||
objInfo.QuestionID, objInfo.Title, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
|
||||
} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
|
||||
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
|
||||
objInfo.ObjectCreatorUserID, comment.ID, comment.UserID, htmltext.FetchExcerpt(comment.ParsedText, "...", 240))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (cs *ReviewService) notificationCommentReply(ctx context.Context, replyUserID, commentID, commentUserID,
|
||||
questionID, questionTitle, commentSummary string) {
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: replyUserID,
|
||||
TriggerUserID: commentUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.NotificationReplyToYou
|
||||
cs.notificationQueueService.Send(ctx, msg)
|
||||
|
||||
// Send external notification.
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, replyUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", replyUserID)
|
||||
return
|
||||
}
|
||||
externalNotificationMsg := &schema.ExternalNotificationMsg{
|
||||
ReceiverUserID: receiverUserInfo.ID,
|
||||
ReceiverEmail: receiverUserInfo.EMail,
|
||||
ReceiverLang: receiverUserInfo.Language,
|
||||
}
|
||||
rawData := &schema.NewCommentTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
CommentID: commentID,
|
||||
CommentSummary: commentSummary,
|
||||
UnsubscribeCode: token.GenerateToken(),
|
||||
}
|
||||
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
|
||||
if commentUser != nil {
|
||||
rawData.CommentUserDisplayName = commentUser.DisplayName
|
||||
}
|
||||
externalNotificationMsg.NewCommentTemplateRawData = rawData
|
||||
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
|
||||
}
|
||||
|
||||
func (cs *ReviewService) notificationMention(
|
||||
ctx context.Context, mentionUsernameList []string, commentID, commentUserID string,
|
||||
alreadyNotifiedUserID map[string]bool) (alreadyNotifiedUserIDs []string) {
|
||||
for _, username := range mentionUsernameList {
|
||||
userInfo, exist, err := cs.userCommon.GetUserBasicInfoByUserName(ctx, username)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
continue
|
||||
}
|
||||
if exist && !alreadyNotifiedUserID[userInfo.ID] {
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: userInfo.ID,
|
||||
TriggerUserID: commentUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.NotificationMentionYou
|
||||
cs.notificationQueueService.Send(ctx, msg)
|
||||
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
|
||||
}
|
||||
}
|
||||
return alreadyNotifiedUserIDs
|
||||
}
|
||||
|
||||
func (cs *ReviewService) notificationQuestionComment(ctx context.Context, questionUserID,
|
||||
questionID, questionTitle, commentID, commentUserID, commentSummary string) {
|
||||
if questionUserID == commentUserID {
|
||||
return
|
||||
}
|
||||
// send internal notification
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: questionUserID,
|
||||
TriggerUserID: commentUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.NotificationCommentQuestion
|
||||
cs.notificationQueueService.Send(ctx, msg)
|
||||
|
||||
// send external notification
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, questionUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", questionUserID)
|
||||
return
|
||||
}
|
||||
|
||||
externalNotificationMsg := &schema.ExternalNotificationMsg{
|
||||
ReceiverUserID: receiverUserInfo.ID,
|
||||
ReceiverEmail: receiverUserInfo.EMail,
|
||||
ReceiverLang: receiverUserInfo.Language,
|
||||
}
|
||||
rawData := &schema.NewCommentTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
CommentID: commentID,
|
||||
CommentSummary: commentSummary,
|
||||
UnsubscribeCode: token.GenerateToken(),
|
||||
}
|
||||
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
|
||||
if commentUser != nil {
|
||||
rawData.CommentUserDisplayName = commentUser.DisplayName
|
||||
}
|
||||
externalNotificationMsg.NewCommentTemplateRawData = rawData
|
||||
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
|
||||
}
|
||||
|
||||
func (cs *ReviewService) notificationAnswerComment(ctx context.Context,
|
||||
questionID, questionTitle, answerID, answerUserID, commentID, commentUserID, commentSummary string) {
|
||||
if answerUserID == commentUserID {
|
||||
return
|
||||
}
|
||||
|
||||
// Send internal notification.
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: answerUserID,
|
||||
TriggerUserID: commentUserID,
|
||||
Type: schema.NotificationTypeInbox,
|
||||
ObjectID: commentID,
|
||||
}
|
||||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.NotificationCommentAnswer
|
||||
cs.notificationQueueService.Send(ctx, msg)
|
||||
|
||||
// Send external notification.
|
||||
receiverUserInfo, exist, err := cs.userRepo.GetByUserID(ctx, answerUserID)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
log.Warnf("user %s not found", answerUserID)
|
||||
return
|
||||
}
|
||||
externalNotificationMsg := &schema.ExternalNotificationMsg{
|
||||
ReceiverUserID: receiverUserInfo.ID,
|
||||
ReceiverEmail: receiverUserInfo.EMail,
|
||||
ReceiverLang: receiverUserInfo.Language,
|
||||
}
|
||||
rawData := &schema.NewCommentTemplateRawData{
|
||||
QuestionTitle: questionTitle,
|
||||
QuestionID: questionID,
|
||||
AnswerID: answerID,
|
||||
CommentID: commentID,
|
||||
CommentSummary: commentSummary,
|
||||
UnsubscribeCode: token.GenerateToken(),
|
||||
}
|
||||
commentUser, _, _ := cs.userCommon.GetUserBasicInfoByID(ctx, commentUserID)
|
||||
if commentUser != nil {
|
||||
rawData.CommentUserDisplayName = commentUser.DisplayName
|
||||
}
|
||||
externalNotificationMsg.NewCommentTemplateRawData = rawData
|
||||
cs.externalNotificationQueueService.Send(ctx, externalNotificationMsg)
|
||||
}
|
||||
|
||||
// GetReviewPendingCount get review pending count
|
||||
func (cs *ReviewService) GetReviewPendingCount(ctx context.Context) (count int64, err error) {
|
||||
return cs.reviewRepo.GetReviewCount(ctx, entity.ReviewStatusPending)
|
||||
|
||||
@@ -27,7 +27,9 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/apache/answer/internal/base/constant"
|
||||
"github.com/apache/answer/internal/base/handler"
|
||||
"github.com/apache/answer/internal/base/reason"
|
||||
"github.com/apache/answer/internal/base/translator"
|
||||
"github.com/apache/answer/internal/base/validator"
|
||||
"github.com/apache/answer/internal/entity"
|
||||
"github.com/apache/answer/internal/schema"
|
||||
@@ -292,6 +294,15 @@ func (ts *TagCommonService) ExistRecommend(ctx context.Context, tags []*schema.T
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (ts *TagCommonService) GetMinimumTags(ctx context.Context) (int, error) {
|
||||
siteInfo, err := ts.siteInfoService.GetSiteWrite(ctx)
|
||||
if err != nil {
|
||||
return 1, err
|
||||
}
|
||||
minimumTags := siteInfo.MinimumTags
|
||||
return minimumTags, nil
|
||||
}
|
||||
|
||||
func (ts *TagCommonService) HasNewTag(ctx context.Context, tags []*schema.TagItem) (bool, error) {
|
||||
tagNames := make([]string, 0)
|
||||
tagMap := make(map[string]bool)
|
||||
@@ -648,9 +659,18 @@ func (ts *TagCommonService) CheckChangeReservedTag(ctx context.Context, oldobjec
|
||||
}
|
||||
|
||||
// ObjectChangeTag change object tag list
|
||||
func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *schema.TagChange) (err error) {
|
||||
if len(objectTagData.Tags) == 0 {
|
||||
return nil
|
||||
func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *schema.TagChange, minimumTags int) (errorlist []*validator.FormErrorField, err error) {
|
||||
//checks if the tags sent in the put req are less than the minimum, if so, tag changes are not applied
|
||||
if len(objectTagData.Tags) < minimumTags {
|
||||
|
||||
errorlist := make([]*validator.FormErrorField, 0)
|
||||
errorlist = append(errorlist, &validator.FormErrorField{
|
||||
ErrorField: "tags",
|
||||
ErrorMsg: translator.Tr(handler.GetLangByCtx(ctx), reason.TagMinCount),
|
||||
})
|
||||
|
||||
err = errors.BadRequest(reason.TagMinCount)
|
||||
return errorlist, err
|
||||
}
|
||||
|
||||
thisObjTagNameList := make([]string, 0)
|
||||
@@ -663,7 +683,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
||||
// find tags name
|
||||
tagListInDb, err := ts.tagCommonRepo.GetTagListByNames(ctx, thisObjTagNameList)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tagInDbMapping := make(map[string]*entity.Tag)
|
||||
@@ -691,7 +711,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
||||
if len(addTagList) > 0 {
|
||||
err = ts.tagCommonRepo.AddTagList(ctx, addTagList)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
for _, tag := range addTagList {
|
||||
thisObjTagIDList = append(thisObjTagIDList, tag.ID)
|
||||
@@ -704,7 +724,7 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
||||
revisionDTO.Content = string(tagInfoJson)
|
||||
revisionID, err := ts.revisionService.AddRevision(ctx, revisionDTO, true)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
ts.activityQueueService.Send(ctx, &schema.ActivityMsg{
|
||||
UserID: objectTagData.UserID,
|
||||
@@ -718,9 +738,9 @@ func (ts *TagCommonService) ObjectChangeTag(ctx context.Context, objectTagData *
|
||||
|
||||
err = ts.CreateOrUpdateTagRelList(ctx, objectTagData.ObjectID, thisObjTagIDList)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (ts *TagCommonService) CountTagRelByTagID(ctx context.Context, tagID string) (count int64, err error) {
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/apache/answer/configs"
|
||||
"github.com/apache/answer/internal/cli"
|
||||
"github.com/apache/answer/internal/base/path"
|
||||
"github.com/apache/answer/pkg/dir"
|
||||
)
|
||||
|
||||
@@ -36,7 +36,7 @@ var (
|
||||
)
|
||||
|
||||
func initReservedUsername() {
|
||||
reservedUsernamesJsonFilePath := filepath.Join(cli.ConfigFileDir, cli.DefaultReservedUsernamesConfigFileName)
|
||||
reservedUsernamesJsonFilePath := filepath.Join(path.ConfigFileDir, path.DefaultReservedUsernamesConfigFileName)
|
||||
if dir.CheckFileExist(reservedUsernamesJsonFilePath) {
|
||||
// if reserved username file exists, read it and replace configuration
|
||||
reservedUsernamesJsonFile, err := os.ReadFile(reservedUsernamesJsonFilePath)
|
||||
|
||||
+18
-1
@@ -19,8 +19,25 @@
|
||||
|
||||
package converter
|
||||
|
||||
import "github.com/segmentfault/pacman/utils"
|
||||
import (
|
||||
"regexp"
|
||||
|
||||
"github.com/segmentfault/pacman/utils"
|
||||
)
|
||||
|
||||
func DeleteUserDisplay(userID string) string {
|
||||
return utils.EnShortID(StringToInt64(userID), 100)
|
||||
}
|
||||
|
||||
func GetMentionUsernameList(text string) []string {
|
||||
re := regexp.MustCompile(`\[@([^\]]+)\]\(/users/[^\)]+\)`)
|
||||
matches := re.FindAllStringSubmatch(text, -1)
|
||||
|
||||
var usernames []string
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
usernames = append(usernames, match[1])
|
||||
}
|
||||
}
|
||||
return usernames
|
||||
}
|
||||
|
||||
+20
-10
@@ -23,16 +23,17 @@ type ConfigType string
|
||||
type InputType string
|
||||
|
||||
const (
|
||||
ConfigTypeInput ConfigType = "input"
|
||||
ConfigTypeTextarea ConfigType = "textarea"
|
||||
ConfigTypeCheckbox ConfigType = "checkbox"
|
||||
ConfigTypeRadio ConfigType = "radio"
|
||||
ConfigTypeSelect ConfigType = "select"
|
||||
ConfigTypeUpload ConfigType = "upload"
|
||||
ConfigTypeTimezone ConfigType = "timezone"
|
||||
ConfigTypeSwitch ConfigType = "switch"
|
||||
ConfigTypeButton ConfigType = "button"
|
||||
ConfigTypeLegend ConfigType = "legend"
|
||||
ConfigTypeInput ConfigType = "input"
|
||||
ConfigTypeTextarea ConfigType = "textarea"
|
||||
ConfigTypeCheckbox ConfigType = "checkbox"
|
||||
ConfigTypeRadio ConfigType = "radio"
|
||||
ConfigTypeSelect ConfigType = "select"
|
||||
ConfigTypeUpload ConfigType = "upload"
|
||||
ConfigTypeTimezone ConfigType = "timezone"
|
||||
ConfigTypeSwitch ConfigType = "switch"
|
||||
ConfigTypeButton ConfigType = "button"
|
||||
ConfigTypeLegend ConfigType = "legend"
|
||||
ConfigTypeTagSelector ConfigType = "tag_selector"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -105,6 +106,15 @@ type OnCompleteAction struct {
|
||||
RefreshFormConfig bool `json:"refresh_form_config"`
|
||||
}
|
||||
|
||||
// TagSelectorOption represents a tag option in the tag selector config value field
|
||||
type TagSelectorOption struct {
|
||||
TagID string `json:"tag_id"`
|
||||
SlugName string `json:"slug_name"`
|
||||
DisplayName string `json:"display_name"`
|
||||
Recommend bool `json:"recommend"`
|
||||
Reserved bool `json:"reserved"`
|
||||
}
|
||||
|
||||
type Config interface {
|
||||
Base
|
||||
|
||||
|
||||
@@ -126,6 +126,10 @@ func Register(p Base) {
|
||||
if _, ok := p.(KVStorage); ok {
|
||||
registerKVStorage(p.(KVStorage))
|
||||
}
|
||||
|
||||
if _, ok := p.(Sidebar); ok {
|
||||
registerSidebar(p.(Sidebar))
|
||||
}
|
||||
}
|
||||
|
||||
type Stack[T Base] struct {
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
// Package plugin
|
||||
package plugin
|
||||
|
||||
type SidebarConfig struct {
|
||||
Tags []*TagSelectorOption `json:"tags"`
|
||||
LinksText string `json:"links_text"`
|
||||
}
|
||||
|
||||
type Sidebar interface {
|
||||
Base
|
||||
GetSidebarConfig() (sidebarConfig *SidebarConfig, err error)
|
||||
}
|
||||
|
||||
var (
|
||||
// CallRender is a function that calls all registered parsers
|
||||
CallSidebar,
|
||||
registerSidebar = MakePlugin[Sidebar](false)
|
||||
)
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
github.com/apache/answer-plugins/connector-basic@latest
|
||||
github.com/apache/answer-plugins/reviewer-basic@latest
|
||||
github.com/apache/answer-plugins/captcha-basic@latest
|
||||
github.com/apache/answer-plugins/captcha-basic@latest
|
||||
github.com/apache/answer-plugins/quick-links@latest
|
||||
@@ -3,3 +3,4 @@ ESLINT_NO_DEV_ERRORS=true
|
||||
PUBLIC_URL=/
|
||||
REACT_APP_API_URL=/
|
||||
REACT_APP_BASE_URL=
|
||||
REACT_APP_API_BASE_URL=
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
--an-toolbar-divider: rgba(0, 0, 0, 0.1);
|
||||
--an-ced4da: #ced4da;
|
||||
--an-e9ecef: #e9ecef;
|
||||
--an-pre: #161b22;
|
||||
--an-pre: #f8f9fa;
|
||||
--an-6c757d: #6c757d;
|
||||
--an-212529: #212529;
|
||||
--an-gray-300: var(--bs-gray-300);
|
||||
|
||||
@@ -439,6 +439,8 @@ export interface AdminSettingsLegal {
|
||||
|
||||
export interface AdminSettingsWrite {
|
||||
restrict_answer?: boolean;
|
||||
min_tags?: number;
|
||||
min_content?: number;
|
||||
recommend_tags?: Tag[];
|
||||
required_tag?: boolean;
|
||||
reserved_tags?: Tag[];
|
||||
|
||||
@@ -35,6 +35,7 @@
|
||||
padding-left: 12px;
|
||||
padding-right: 12px;
|
||||
}
|
||||
|
||||
.page-main {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,9 @@ const ActionBar = ({
|
||||
}`}
|
||||
onClick={onVote}>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
{voteCount > 0 && <span className="ms-2">{voteCount}</span>}
|
||||
{voteCount > 0 && (
|
||||
<span className="ms-2 link-secondary">{voteCount}</span>
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { FC, useState, useEffect } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Link } from 'react-router-dom';
|
||||
@@ -50,7 +50,14 @@ import { Form, ActionBar, Reply } from './components';
|
||||
|
||||
import './index.scss';
|
||||
|
||||
const Comment = ({ objectId, mode, commentId }) => {
|
||||
interface IProps {
|
||||
objectId: string;
|
||||
mode?: 'answer' | 'question';
|
||||
commentId?: string;
|
||||
children?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Comment: FC<IProps> = ({ objectId, mode, commentId, children }) => {
|
||||
const pageUsers = usePageUsers();
|
||||
const [pageIndex, setPageIndex] = useState(0);
|
||||
const [visibleComment, setVisibleComment] = useState(false);
|
||||
@@ -374,11 +381,18 @@ const Comment = ({ objectId, mode, commentId }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Reactions
|
||||
objectId={objectId}
|
||||
showAddCommentBtn={comments.length === 0}
|
||||
handleClickComment={handleAddComment}
|
||||
/>
|
||||
<div
|
||||
className={classNames(
|
||||
'd-flex flex-wrap justify-content-between align-items-center',
|
||||
comments.length === 0 ? '' : 'mb-3',
|
||||
)}>
|
||||
<Reactions
|
||||
objectId={objectId}
|
||||
showAddCommentBtn={comments.length === 0}
|
||||
handleClickComment={handleAddComment}
|
||||
/>
|
||||
{children}
|
||||
</div>
|
||||
<div
|
||||
className={classNames(
|
||||
'comments-wrap',
|
||||
@@ -403,11 +417,18 @@ const Comment = ({ objectId, mode, commentId }) => {
|
||||
/>
|
||||
) : (
|
||||
<div className="d-block">
|
||||
{item.reply_user_display_name && (
|
||||
<Link to="." className="small me-1 text-nowrap">
|
||||
@{item.reply_user_display_name}
|
||||
</Link>
|
||||
)}
|
||||
{item.reply_user_display_name &&
|
||||
(item.reply_user_status !== 'deleted' ? (
|
||||
<Link
|
||||
to={`/users/${item.reply_username}`}
|
||||
className="small me-1 text-nowrap">
|
||||
@{item.reply_user_display_name}
|
||||
</Link>
|
||||
) : (
|
||||
<span className="small me-1 text-nowrap">
|
||||
@{item.reply_user_display_name}
|
||||
</span>
|
||||
))}
|
||||
|
||||
<div
|
||||
className="fmt small text-break text-wrap"
|
||||
|
||||
@@ -32,7 +32,7 @@ const Index = () => {
|
||||
const cc = `${fullYear} ${siteName}`;
|
||||
|
||||
return (
|
||||
<footer className="py-3 bg-light w-100">
|
||||
<footer className="py-3 w-100">
|
||||
<p className="text-center mb-0 small">
|
||||
{/* Link to Terms of Service with right margin */}
|
||||
<Link to="/tos" className="me-3">
|
||||
|
||||
@@ -123,7 +123,7 @@ const Index: FC<Props> = ({ redDot, userInfo, logOut }) => {
|
||||
{ucAgent?.enabled &&
|
||||
(ucAgent?.agent_info?.url ||
|
||||
ucAgent?.agent_info?.control_center?.length) ? (
|
||||
<Dropdown align="end">
|
||||
<Dropdown align="end" data-bs-theme={isDarkTheme() ? 'dark' : 'light'}>
|
||||
<Dropdown.Toggle
|
||||
variant="success"
|
||||
id="dropdown-uca"
|
||||
|
||||
@@ -57,7 +57,7 @@ const Header: FC = () => {
|
||||
* Automatically append `tag` information when creating a question
|
||||
*/
|
||||
const tagMatch = useMatch('/tags/:slugName');
|
||||
let askUrl = '/questions/ask';
|
||||
let askUrl = '/questions/add';
|
||||
if (tagMatch && tagMatch.params.slugName) {
|
||||
askUrl = `${askUrl}?tags=${encodeURIComponent(tagMatch.params.slugName)}`;
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import { Button, Dropdown } from 'react-bootstrap';
|
||||
import { Link, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Modal } from '@/components';
|
||||
import { Icon, Modal } from '@/components';
|
||||
import { useReportModal, useToast } from '@/hooks';
|
||||
import { useCaptchaPlugin } from '@/utils/pluginKit';
|
||||
import { QuestionOperationReq } from '@/common/interface';
|
||||
@@ -40,6 +40,8 @@ import { tryNormalLogged } from '@/utils/guard';
|
||||
import { floppyNavigation } from '@/utils';
|
||||
import { toastStore } from '@/stores';
|
||||
|
||||
import '@/components/QueryGroup/index.scss';
|
||||
|
||||
interface IProps {
|
||||
type: 'answer' | 'question';
|
||||
qid: string;
|
||||
@@ -334,54 +336,95 @@ const Index: FC<IProps> = ({
|
||||
) || [];
|
||||
|
||||
return (
|
||||
<div className="d-flex align-items-center">
|
||||
<Share type={type} qid={qid} aid={aid} title={title} />
|
||||
{firstAction?.map((item) => {
|
||||
if (item.action === 'edit') {
|
||||
<>
|
||||
<div className="md-show align-items-center">
|
||||
<Share
|
||||
type={type}
|
||||
qid={qid}
|
||||
aid={aid}
|
||||
title={title}
|
||||
className="link-secondary small"
|
||||
/>
|
||||
{firstAction?.map((item) => {
|
||||
if (item.action === 'edit') {
|
||||
return (
|
||||
<Link
|
||||
key={item.action}
|
||||
to={editUrl}
|
||||
className="link-secondary p-0 small ms-3"
|
||||
onClick={(evt) => handleEdit(evt, editUrl)}
|
||||
style={{ lineHeight: '23px' }}>
|
||||
{item.name}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
<Button
|
||||
key={item.action}
|
||||
to={editUrl}
|
||||
className="link-secondary p-0 small ms-3"
|
||||
onClick={(evt) => handleEdit(evt, editUrl)}
|
||||
style={{ lineHeight: '23px' }}>
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="link-secondary p-0 ms-3"
|
||||
onClick={() => handleAction(item.action)}>
|
||||
{item.name}
|
||||
</Link>
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
key={item.action}
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="link-secondary p-0 ms-3"
|
||||
onClick={() => handleAction(item.action)}>
|
||||
{item.name}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{secondAction.length > 0 && (
|
||||
<Dropdown className="ms-3 d-flex">
|
||||
<Dropdown.Toggle
|
||||
variant="link"
|
||||
size="sm"
|
||||
className="link-secondary p-0 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>
|
||||
})}
|
||||
{secondAction.length > 0 && (
|
||||
<Dropdown className="ms-3 d-flex">
|
||||
<Dropdown.Toggle
|
||||
variant="link"
|
||||
size="sm"
|
||||
title={t('action', { keyPrefix: 'question_detail' })}
|
||||
className="link-secondary p-0 no-toggle">
|
||||
<Icon name="three-dots" />
|
||||
</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 className="md-hide">
|
||||
{memberActions.length > 0 && (
|
||||
<Dropdown className="d-flex">
|
||||
<Dropdown.Toggle
|
||||
variant="link"
|
||||
size="sm"
|
||||
title={t('action', { keyPrefix: 'question_detail' })}
|
||||
className="link-secondary no-toggle">
|
||||
<Icon name="three-dots" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu>
|
||||
<Share
|
||||
type={type}
|
||||
qid={qid}
|
||||
aid={aid}
|
||||
title={title}
|
||||
className="inherit"
|
||||
mode="mobile"
|
||||
/>
|
||||
{[...firstAction, ...secondAction].map((item) => {
|
||||
return (
|
||||
<Dropdown.Item
|
||||
key={item.action}
|
||||
onClick={() => handleAction(item.action)}>
|
||||
{item.name}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
})}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*/
|
||||
|
||||
import { FC } from 'react';
|
||||
|
||||
import { TagSelector } from '@/components';
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
interface Props {
|
||||
maxTagLength?: number;
|
||||
description?: string;
|
||||
fieldName: string;
|
||||
onChange?: (fd: Type.FormDataType) => void;
|
||||
formData: Type.FormDataType;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
description,
|
||||
maxTagLength,
|
||||
fieldName,
|
||||
onChange,
|
||||
formData,
|
||||
}) => {
|
||||
const fieldObject = formData[fieldName];
|
||||
const handleChange = (data: Type.Tag[]) => {
|
||||
const state = {
|
||||
...formData,
|
||||
[fieldName]: {
|
||||
...formData[fieldName],
|
||||
value: data,
|
||||
isInvalid: false,
|
||||
},
|
||||
};
|
||||
if (typeof onChange === 'function') {
|
||||
onChange(state);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<TagSelector
|
||||
value={fieldObject?.value || []}
|
||||
onChange={handleChange}
|
||||
maxTagLength={maxTagLength || 0}
|
||||
isInvalid={fieldObject?.isInvalid}
|
||||
formText={description}
|
||||
errMsg={fieldObject?.errorMsg}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -27,6 +27,7 @@ import Textarea from './Textarea';
|
||||
import Input from './Input';
|
||||
import Button from './Button';
|
||||
import InputGroup from './InputGroup';
|
||||
import TagSelector from './TagSelector';
|
||||
|
||||
export {
|
||||
Legend,
|
||||
@@ -39,4 +40,5 @@ export {
|
||||
Input,
|
||||
Button,
|
||||
InputGroup,
|
||||
TagSelector,
|
||||
};
|
||||
|
||||
@@ -51,6 +51,7 @@ import {
|
||||
Input,
|
||||
Button as SfButton,
|
||||
InputGroup,
|
||||
TagSelector,
|
||||
} from './components';
|
||||
|
||||
export * from './types';
|
||||
@@ -258,6 +259,7 @@ const SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (
|
||||
description,
|
||||
enum: enumValues = [],
|
||||
enumNames = [],
|
||||
max_length = 0,
|
||||
} = properties[key];
|
||||
const { 'ui:widget': widget = 'input', 'ui:options': uiOpt } =
|
||||
uiSchema?.[key] || {};
|
||||
@@ -413,11 +415,20 @@ const SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (
|
||||
/>
|
||||
</InputGroup>
|
||||
) : null}
|
||||
{widget === 'tag_selector' ? (
|
||||
<TagSelector
|
||||
maxTagLength={max_length}
|
||||
fieldName={key}
|
||||
onChange={onChange}
|
||||
formData={formData}
|
||||
description={description}
|
||||
/>
|
||||
) : null}
|
||||
{/* Unified handling of `Feedback` and `Text` */}
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{fieldState?.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
{description ? (
|
||||
{description && widget !== 'tag_selector' ? (
|
||||
<Form.Text dangerouslySetInnerHTML={{ __html: description }} />
|
||||
) : null}
|
||||
</Form.Group>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user