mirror of https://gitee.com/answerdev/answer.git
Merge branch 'release/1.0.4' into 'main'
Release/1.0.4 See merge request opensource/answer!437
This commit is contained in:
commit
f93fb6b709
|
@ -0,0 +1,41 @@
|
|||
name: "Goreleaser"
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "v*"
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: [self-hosted, linux]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: Set up Node
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16
|
||||
|
||||
- name: Node Build
|
||||
run: make install-ui-packages ui
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.19
|
||||
- name: Run GoReleaser
|
||||
uses: goreleaser/goreleaser-action@v4
|
||||
with:
|
||||
distribution: goreleaser
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: answer
|
||||
path: ./dist/*
|
|
@ -12,7 +12,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
|
|
@ -17,7 +17,7 @@ env:
|
|||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux]
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
|
|
|
@ -9,7 +9,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: [self-hosted, linux]
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
|
|
|
@ -8,8 +8,7 @@ on:
|
|||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
runs-on: [self-hosted, linux]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
|
|
|
@ -15,8 +15,11 @@
|
|||
/configs/config-dev.yaml
|
||||
/go.work*
|
||||
/logs
|
||||
/ui/build
|
||||
/ui/node_modules
|
||||
/ui/build/*/*/*
|
||||
/ui/build/*.json
|
||||
/ui/build/*.html
|
||||
/ui/build/*.txt
|
||||
/vendor
|
||||
Thumbs*.db
|
||||
tmp
|
||||
|
|
|
@ -2,7 +2,7 @@ env:
|
|||
- GO11MODULE=on
|
||||
- GO111MODULE=on
|
||||
- GOPROXY=https://goproxy.io
|
||||
- CGO_ENABLED=1
|
||||
- CGO_ENABLED=0
|
||||
|
||||
before:
|
||||
hooks:
|
||||
|
@ -16,74 +16,48 @@ builds:
|
|||
- linux
|
||||
goarch:
|
||||
- amd64
|
||||
# linux windows need cgomingw64-gcc
|
||||
- id: build-windows
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
env:
|
||||
- CC=x86_64-w64-mingw32-gcc
|
||||
- CXX=x86_64-w64-mingw32-g++
|
||||
goos:
|
||||
- windows
|
||||
goarch:
|
||||
- amd64
|
||||
# linux arm64 need cgo arm64
|
||||
- id: build-arm64
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
env:
|
||||
- CC=aarch64-linux-gnu-gcc
|
||||
- CXX=aarch64-linux-gnu-g++
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm64
|
||||
- id: build-arm7
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
env:
|
||||
- CC=arm-linux-gnueabihf-gcc
|
||||
- CXX=arm-linux-gnueabihf-g++
|
||||
- AR=arm-linux-gnueabihf-ar
|
||||
goos:
|
||||
- linux
|
||||
goarch:
|
||||
- arm
|
||||
goarm:
|
||||
- 7
|
||||
- arm64
|
||||
- id: build-darwin-arm64
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
env:
|
||||
- CC=oa64-clang
|
||||
- CXX=oa64-clang++
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- arm64
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
flags: -v
|
||||
- id: build-darwin-amd64
|
||||
main: ./cmd/answer/.
|
||||
binary: answer
|
||||
env:
|
||||
- CC=o64-clang
|
||||
- CXX=o64-clang++
|
||||
goos:
|
||||
- darwin
|
||||
goarch:
|
||||
- amd64
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
ldflags: -s -w -X main.Version={{.Version}} -X main.Revision={{.ShortCommit}} -X main.Time={{.Date}} -X main.BuildUser=goreleaser
|
||||
flags: -v
|
||||
|
||||
|
||||
archives:
|
||||
- replacements:
|
||||
darwin: Darwin
|
||||
darwin: macOS
|
||||
amd64: x86_64
|
||||
linux: Linux
|
||||
windows: Windows
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
|
@ -95,10 +69,4 @@ changelog:
|
|||
- '^docs:'
|
||||
- '^test:'
|
||||
|
||||
|
||||
# sudo apt-get install build-essential
|
||||
# sudo apt-get install gcc-multilib g++-multilib
|
||||
# sudo apt-get install gcc-mingw-w64
|
||||
# sudo apt-get -y install gcc-aarch64-linux-gnu gcc-arm-linux-gnueabihf
|
||||
# sudo apt-get install clang llvm
|
||||
# goreleaser release --snapshot --rm-dist
|
||||
# goreleaser release --snapshot --rm-dist
|
||||
|
|
22
Makefile
22
Makefile
|
@ -1,30 +1,32 @@
|
|||
.PHONY: build clean ui
|
||||
|
||||
VERSION=1.0.3
|
||||
VERSION=1.0.4
|
||||
BIN=answer
|
||||
DIR_SRC=./cmd/answer
|
||||
DOCKER_CMD=docker
|
||||
|
||||
#GO_ENV=CGO_ENABLED=0
|
||||
GO_ENV=CGO_ENABLED=0 GO111MODULE=on
|
||||
Revision=$(shell git rev-parse --short HEAD)
|
||||
GO_FLAGS=-ldflags="-X main.Version=$(VERSION) -X 'main.Revision=$(Revision)' -X 'main.Time=`date`' -extldflags -static"
|
||||
GO=$(GO_ENV) $(shell which go)
|
||||
|
||||
build:
|
||||
@$(GO_ENV) $(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
|
||||
build: generate
|
||||
@$(GO) build $(GO_FLAGS) -o $(BIN) $(DIR_SRC)
|
||||
|
||||
# https://dev.to/thewraven/universal-macos-binaries-with-go-1-16-3mm3
|
||||
universal:
|
||||
universal: generate
|
||||
@GOOS=darwin GOARCH=amd64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_amd64 $(DIR_SRC)
|
||||
@GOOS=darwin GOARCH=arm64 $(GO_ENV) $(GO) build $(GO_FLAGS) -o ${BIN}_arm64 $(DIR_SRC)
|
||||
@lipo -create -output ${BIN} ${BIN}_amd64 ${BIN}_arm64
|
||||
@rm -f ${BIN}_amd64 ${BIN}_arm64
|
||||
|
||||
generate:
|
||||
go get github.com/google/wire/cmd/wire@latest
|
||||
go install github.com/golang/mock/mockgen@v1.6.0
|
||||
go generate ./...
|
||||
go mod tidy
|
||||
@$(GO) get github.com/google/wire/cmd/wire@v0.5.0
|
||||
@$(GO) get github.com/golang/mock/mockgen@v1.6.0
|
||||
@$(GO) install github.com/google/wire/cmd/wire@v0.5.0
|
||||
@$(GO) install github.com/golang/mock/mockgen@v1.6.0
|
||||
@$(GO) generate ./...
|
||||
@$(GO) mod tidy
|
||||
|
||||
test:
|
||||
@$(GO) test ./internal/repo/repo_test
|
||||
|
@ -39,6 +41,6 @@ install-ui-packages:
|
|||
@corepack prepare pnpm@v7.12.2 --activate
|
||||
|
||||
ui:
|
||||
@cd ui && pnpm install && pnpm build && cd -
|
||||
@cd ui && pnpm install && pnpm build && sed -i 's/%AnswerVersion%/'$(VERSION)'/g' ./build/index.html && cd -
|
||||
|
||||
all: clean build
|
||||
|
|
128
docs/docs.go
128
docs/docs.go
|
@ -2911,6 +2911,45 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/post/render": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "render post content",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"summary": "render post content",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "PostRenderReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.PostRenderReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -5425,8 +5464,7 @@ const docTemplate = `{
|
|||
"type": "object",
|
||||
"required": [
|
||||
"object_id",
|
||||
"original_text",
|
||||
"parsed_text"
|
||||
"original_text"
|
||||
],
|
||||
"properties": {
|
||||
"mention_username_list": {
|
||||
|
@ -5442,11 +5480,9 @@ const docTemplate = `{
|
|||
},
|
||||
"original_text": {
|
||||
"description": "original comment content",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed comment content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 600,
|
||||
"minLength": 2
|
||||
},
|
||||
"reply_comment_id": {
|
||||
"description": "reply comment id",
|
||||
|
@ -5535,46 +5571,41 @@ const docTemplate = `{
|
|||
},
|
||||
"schema.AnswerAddReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"question_id": {
|
||||
"description": "question_id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.AnswerUpdateReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"edit_summary": {
|
||||
"description": "edit_summary",
|
||||
"type": "string"
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "id",
|
||||
"type": "string"
|
||||
},
|
||||
"question_id": {
|
||||
"description": "question_id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "title",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@ -6589,11 +6620,18 @@ const docTemplate = `{
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.PostRenderReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionAdd": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content",
|
||||
"html",
|
||||
"tags",
|
||||
"title"
|
||||
],
|
||||
|
@ -6604,12 +6642,6 @@ const docTemplate = `{
|
|||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"tags": {
|
||||
"description": "tags",
|
||||
"type": "array",
|
||||
|
@ -6669,6 +6701,9 @@ const docTemplate = `{
|
|||
"collection_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -6739,7 +6774,6 @@ const docTemplate = `{
|
|||
"type": "object",
|
||||
"required": [
|
||||
"content",
|
||||
"html",
|
||||
"id",
|
||||
"tags",
|
||||
"title"
|
||||
|
@ -6755,12 +6789,6 @@ const docTemplate = `{
|
|||
"description": "edit summary",
|
||||
"type": "string"
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"id": {
|
||||
"description": "question id",
|
||||
"type": "string"
|
||||
|
@ -7326,10 +7354,6 @@ const docTemplate = `{
|
|||
"description": "original text",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed text",
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"description": "slug_name",
|
||||
"type": "string",
|
||||
|
@ -7416,7 +7440,8 @@ const docTemplate = `{
|
|||
"schema.UpdateCommentReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment_id"
|
||||
"comment_id",
|
||||
"original_text"
|
||||
],
|
||||
"properties": {
|
||||
"comment_id": {
|
||||
|
@ -7425,11 +7450,9 @@ const docTemplate = `{
|
|||
},
|
||||
"original_text": {
|
||||
"description": "original comment content",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed comment content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 600,
|
||||
"minLength": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7460,11 +7483,6 @@ const docTemplate = `{
|
|||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"bio_html": {
|
||||
"description": "bio",
|
||||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string",
|
||||
|
@ -7549,10 +7567,6 @@ const docTemplate = `{
|
|||
"description": "original text",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed text",
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"description": "slug_name",
|
||||
"type": "string",
|
||||
|
|
|
@ -2899,6 +2899,45 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/post/render": {
|
||||
"post": {
|
||||
"security": [
|
||||
{
|
||||
"ApiKeyAuth": []
|
||||
}
|
||||
],
|
||||
"description": "render post content",
|
||||
"consumes": [
|
||||
"application/json"
|
||||
],
|
||||
"produces": [
|
||||
"application/json"
|
||||
],
|
||||
"tags": [
|
||||
"Upload"
|
||||
],
|
||||
"summary": "render post content",
|
||||
"parameters": [
|
||||
{
|
||||
"description": "PostRenderReq",
|
||||
"name": "data",
|
||||
"in": "body",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"$ref": "#/definitions/schema.PostRenderReq"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "OK",
|
||||
"schema": {
|
||||
"$ref": "#/definitions/handler.RespBody"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/answer/api/v1/question": {
|
||||
"put": {
|
||||
"security": [
|
||||
|
@ -5413,8 +5452,7 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"object_id",
|
||||
"original_text",
|
||||
"parsed_text"
|
||||
"original_text"
|
||||
],
|
||||
"properties": {
|
||||
"mention_username_list": {
|
||||
|
@ -5430,11 +5468,9 @@
|
|||
},
|
||||
"original_text": {
|
||||
"description": "original comment content",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed comment content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 600,
|
||||
"minLength": 2
|
||||
},
|
||||
"reply_comment_id": {
|
||||
"description": "reply comment id",
|
||||
|
@ -5523,46 +5559,41 @@
|
|||
},
|
||||
"schema.AnswerAddReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"question_id": {
|
||||
"description": "question_id",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.AnswerUpdateReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content"
|
||||
],
|
||||
"properties": {
|
||||
"content": {
|
||||
"description": "content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"edit_summary": {
|
||||
"description": "edit_summary",
|
||||
"type": "string"
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"description": "id",
|
||||
"type": "string"
|
||||
},
|
||||
"question_id": {
|
||||
"description": "question_id",
|
||||
"type": "string"
|
||||
},
|
||||
"title": {
|
||||
"description": "title",
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
|
@ -6577,11 +6608,18 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"schema.PostRenderReq": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"content": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"schema.QuestionAdd": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"content",
|
||||
"html",
|
||||
"tags",
|
||||
"title"
|
||||
],
|
||||
|
@ -6592,12 +6630,6 @@
|
|||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"tags": {
|
||||
"description": "tags",
|
||||
"type": "array",
|
||||
|
@ -6657,6 +6689,9 @@
|
|||
"collection_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"created_at": {
|
||||
"type": "integer"
|
||||
},
|
||||
"description": {
|
||||
"type": "string"
|
||||
},
|
||||
|
@ -6727,7 +6762,6 @@
|
|||
"type": "object",
|
||||
"required": [
|
||||
"content",
|
||||
"html",
|
||||
"id",
|
||||
"tags",
|
||||
"title"
|
||||
|
@ -6743,12 +6777,6 @@
|
|||
"description": "edit summary",
|
||||
"type": "string"
|
||||
},
|
||||
"html": {
|
||||
"description": "html",
|
||||
"type": "string",
|
||||
"maxLength": 65535,
|
||||
"minLength": 6
|
||||
},
|
||||
"id": {
|
||||
"description": "question id",
|
||||
"type": "string"
|
||||
|
@ -7314,10 +7342,6 @@
|
|||
"description": "original text",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed text",
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"description": "slug_name",
|
||||
"type": "string",
|
||||
|
@ -7404,7 +7428,8 @@
|
|||
"schema.UpdateCommentReq": {
|
||||
"type": "object",
|
||||
"required": [
|
||||
"comment_id"
|
||||
"comment_id",
|
||||
"original_text"
|
||||
],
|
||||
"properties": {
|
||||
"comment_id": {
|
||||
|
@ -7413,11 +7438,9 @@
|
|||
},
|
||||
"original_text": {
|
||||
"description": "original comment content",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed comment content",
|
||||
"type": "string"
|
||||
"type": "string",
|
||||
"maxLength": 600,
|
||||
"minLength": 2
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -7448,11 +7471,6 @@
|
|||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"bio_html": {
|
||||
"description": "bio",
|
||||
"type": "string",
|
||||
"maxLength": 4096
|
||||
},
|
||||
"display_name": {
|
||||
"description": "display_name",
|
||||
"type": "string",
|
||||
|
@ -7537,10 +7555,6 @@
|
|||
"description": "original text",
|
||||
"type": "string"
|
||||
},
|
||||
"parsed_text": {
|
||||
"description": "parsed text",
|
||||
"type": "string"
|
||||
},
|
||||
"slug_name": {
|
||||
"description": "slug_name",
|
||||
"type": "string",
|
||||
|
|
|
@ -147,9 +147,8 @@ definitions:
|
|||
type: string
|
||||
original_text:
|
||||
description: original comment content
|
||||
type: string
|
||||
parsed_text:
|
||||
description: parsed comment content
|
||||
maxLength: 600
|
||||
minLength: 2
|
||||
type: string
|
||||
reply_comment_id:
|
||||
description: reply comment id
|
||||
|
@ -157,7 +156,6 @@ definitions:
|
|||
required:
|
||||
- object_id
|
||||
- original_text
|
||||
- parsed_text
|
||||
type: object
|
||||
schema.AddReportReq:
|
||||
properties:
|
||||
|
@ -217,35 +215,30 @@ definitions:
|
|||
schema.AnswerAddReq:
|
||||
properties:
|
||||
content:
|
||||
description: content
|
||||
type: string
|
||||
html:
|
||||
description: html
|
||||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
question_id:
|
||||
description: question_id
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
type: object
|
||||
schema.AnswerUpdateReq:
|
||||
properties:
|
||||
content:
|
||||
description: content
|
||||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
edit_summary:
|
||||
description: edit_summary
|
||||
type: string
|
||||
html:
|
||||
description: html
|
||||
type: string
|
||||
id:
|
||||
description: id
|
||||
type: string
|
||||
question_id:
|
||||
description: question_id
|
||||
type: string
|
||||
title:
|
||||
description: title
|
||||
type: string
|
||||
required:
|
||||
- content
|
||||
type: object
|
||||
schema.AvatarInfo:
|
||||
properties:
|
||||
|
@ -976,6 +969,11 @@ definitions:
|
|||
type:
|
||||
type: string
|
||||
type: object
|
||||
schema.PostRenderReq:
|
||||
properties:
|
||||
content:
|
||||
type: string
|
||||
type: object
|
||||
schema.QuestionAdd:
|
||||
properties:
|
||||
content:
|
||||
|
@ -983,11 +981,6 @@ definitions:
|
|||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
html:
|
||||
description: html
|
||||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
tags:
|
||||
description: tags
|
||||
items:
|
||||
|
@ -1000,7 +993,6 @@ definitions:
|
|||
type: string
|
||||
required:
|
||||
- content
|
||||
- html
|
||||
- tags
|
||||
- title
|
||||
type: object
|
||||
|
@ -1036,6 +1028,8 @@ definitions:
|
|||
type: integer
|
||||
collection_count:
|
||||
type: integer
|
||||
created_at:
|
||||
type: integer
|
||||
description:
|
||||
type: string
|
||||
follow_count:
|
||||
|
@ -1090,11 +1084,6 @@ definitions:
|
|||
edit_summary:
|
||||
description: edit summary
|
||||
type: string
|
||||
html:
|
||||
description: html
|
||||
maxLength: 65535
|
||||
minLength: 6
|
||||
type: string
|
||||
id:
|
||||
description: question id
|
||||
type: string
|
||||
|
@ -1110,7 +1099,6 @@ definitions:
|
|||
type: string
|
||||
required:
|
||||
- content
|
||||
- html
|
||||
- id
|
||||
- tags
|
||||
- title
|
||||
|
@ -1490,9 +1478,6 @@ definitions:
|
|||
original_text:
|
||||
description: original text
|
||||
type: string
|
||||
parsed_text:
|
||||
description: parsed text
|
||||
type: string
|
||||
slug_name:
|
||||
description: slug_name
|
||||
maxLength: 35
|
||||
|
@ -1558,12 +1543,12 @@ definitions:
|
|||
type: string
|
||||
original_text:
|
||||
description: original comment content
|
||||
type: string
|
||||
parsed_text:
|
||||
description: parsed comment content
|
||||
maxLength: 600
|
||||
minLength: 2
|
||||
type: string
|
||||
required:
|
||||
- comment_id
|
||||
- original_text
|
||||
type: object
|
||||
schema.UpdateFollowTagsReq:
|
||||
properties:
|
||||
|
@ -1582,10 +1567,6 @@ definitions:
|
|||
description: bio
|
||||
maxLength: 4096
|
||||
type: string
|
||||
bio_html:
|
||||
description: bio
|
||||
maxLength: 4096
|
||||
type: string
|
||||
display_name:
|
||||
description: display_name
|
||||
maxLength: 30
|
||||
|
@ -1648,9 +1629,6 @@ definitions:
|
|||
original_text:
|
||||
description: original text
|
||||
type: string
|
||||
parsed_text:
|
||||
description: parsed text
|
||||
type: string
|
||||
slug_name:
|
||||
description: slug_name
|
||||
maxLength: 35
|
||||
|
@ -3684,6 +3662,30 @@ paths:
|
|||
summary: user's votes
|
||||
tags:
|
||||
- Activity
|
||||
/answer/api/v1/post/render:
|
||||
post:
|
||||
consumes:
|
||||
- application/json
|
||||
description: render post content
|
||||
parameters:
|
||||
- description: PostRenderReq
|
||||
in: body
|
||||
name: data
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/schema.PostRenderReq'
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"200":
|
||||
description: OK
|
||||
schema:
|
||||
$ref: '#/definitions/handler.RespBody'
|
||||
security:
|
||||
- ApiKeyAuth: []
|
||||
summary: render post content
|
||||
tags:
|
||||
- Upload
|
||||
/answer/api/v1/question:
|
||||
delete:
|
||||
consumes:
|
||||
|
|
18
go.mod
18
go.mod
|
@ -5,6 +5,7 @@ go 1.18
|
|||
require (
|
||||
github.com/Chain-Zhang/pinyin v0.1.3
|
||||
github.com/anargu/gin-brotli v0.0.0-20220116052358-12bf532d5267
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d
|
||||
github.com/bwmarrin/snowflake v0.3.0
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/disintegration/imaging v1.6.2
|
||||
|
@ -14,7 +15,7 @@ require (
|
|||
github.com/go-playground/validator/v10 v10.11.1
|
||||
github.com/go-sql-driver/mysql v1.6.0
|
||||
github.com/goccy/go-json v0.9.11
|
||||
github.com/golang/mock v1.4.4
|
||||
github.com/golang/mock v1.6.0
|
||||
github.com/google/uuid v1.3.0
|
||||
github.com/google/wire v0.5.0
|
||||
github.com/gosimple/slug v1.13.1
|
||||
|
@ -22,7 +23,6 @@ require (
|
|||
github.com/jinzhu/copier v0.3.5
|
||||
github.com/jinzhu/now v1.1.5
|
||||
github.com/lib/pq v1.10.7
|
||||
github.com/mattn/go-sqlite3 v1.14.16
|
||||
github.com/microcosm-cc/bluemonday v1.0.21
|
||||
github.com/mojocn/base64Captcha v1.3.5
|
||||
github.com/ory/dockertest/v3 v3.9.1
|
||||
|
@ -43,6 +43,7 @@ require (
|
|||
golang.org/x/net v0.1.0
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
modernc.org/sqlite v1.14.2
|
||||
xorm.io/builder v0.3.12
|
||||
xorm.io/core v0.7.3
|
||||
xorm.io/xorm v1.3.2
|
||||
|
@ -79,12 +80,14 @@ require (
|
|||
github.com/inconshreveable/mousetrap v1.0.1 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||
github.com/leodido/go-urn v1.2.1 // indirect
|
||||
github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
|
||||
github.com/lestrrat-go/strftime v1.0.6 // indirect
|
||||
github.com/magiconair/properties v1.8.6 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.16 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
|
@ -97,6 +100,7 @@ require (
|
|||
github.com/pelletier/go-toml/v2 v2.0.5 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/spf13/afero v1.9.2 // indirect
|
||||
github.com/spf13/cast v1.5.0 // indirect
|
||||
|
@ -113,6 +117,7 @@ require (
|
|||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.23.0 // indirect
|
||||
golang.org/x/image v0.1.0 // indirect
|
||||
golang.org/x/mod v0.6.0 // indirect
|
||||
golang.org/x/sys v0.1.0 // indirect
|
||||
golang.org/x/text v0.5.0 // indirect
|
||||
golang.org/x/tools v0.2.0 // indirect
|
||||
|
@ -120,5 +125,14 @@ require (
|
|||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
lukechampine.com/uint128 v1.1.1 // indirect
|
||||
modernc.org/cc/v3 v3.35.18 // indirect
|
||||
modernc.org/ccgo/v3 v3.12.82 // indirect
|
||||
modernc.org/libc v1.11.87 // indirect
|
||||
modernc.org/mathutil v1.4.1 // indirect
|
||||
modernc.org/memory v1.0.5 // indirect
|
||||
modernc.org/opt v0.1.1 // indirect
|
||||
modernc.org/strutil v1.1.1 // indirect
|
||||
modernc.org/token v1.0.0 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
|
16
go.sum
16
go.sum
|
@ -79,6 +79,8 @@ github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hC
|
|||
github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
|
||||
github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
|
||||
github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU=
|
||||
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
|
||||
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
|
||||
|
@ -142,6 +144,7 @@ github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5Xh
|
|||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
github.com/docker/go-units v0.4.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
|
||||
github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo=
|
||||
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
|
||||
github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs=
|
||||
github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU=
|
||||
|
@ -237,8 +240,9 @@ github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFU
|
|||
github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
|
||||
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
|
||||
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
|
@ -696,6 +700,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
|
|||
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||
github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
|
@ -793,6 +798,7 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
|||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I=
|
||||
golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
|
@ -832,6 +838,7 @@ golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwY
|
|||
golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210421230115-4e50805a0758/go.mod h1:72T/g9IO56b78aLF+1Kcs5dz7/ng1VjMUvfKvpfy+jM=
|
||||
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
|
@ -912,9 +919,11 @@ golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||
golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210420072515-93ed5bcd2bfe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
|
@ -1009,6 +1018,7 @@ golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4f
|
|||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
|
||||
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.2.0 h1:G6AHpWxTMGY1KyEYoAQ5WTtIekUUvDNjan3ugu60JvE=
|
||||
|
@ -1219,7 +1229,9 @@ modernc.org/ccgo/v3 v3.12.73/go.mod h1:hngkB+nUUqzOf3iqsM48Gf1FZhY599qzVg1iX+BT3
|
|||
modernc.org/ccgo/v3 v3.12.81/go.mod h1:p2A1duHoBBg1mFtYvnhAnQyI6vL0uw5PGYLSIgF6rYY=
|
||||
modernc.org/ccgo/v3 v3.12.82 h1:wudcnJyjLj1aQQCXF3IM9Gz2X6UNjw+afIghzdtn0v8=
|
||||
modernc.org/ccgo/v3 v3.12.82/go.mod h1:ApbflUfa5BKadjHynCficldU1ghjen84tuM5jRynB7w=
|
||||
modernc.org/ccorpus v1.11.1 h1:K0qPfpVG1MJh5BYazccnmhywH4zHuOgJXgbjzyp6dWA=
|
||||
modernc.org/ccorpus v1.11.1/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
|
||||
modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
|
||||
modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
|
||||
modernc.org/libc v1.9.8/go.mod h1:U1eq8YWr/Kc1RWCMFUWEdkTg8OTcfLw2kY8EDwl039w=
|
||||
modernc.org/libc v1.9.11/go.mod h1:NyF3tsA5ArIjJ83XB0JlqhjTabTCHm9aX4XMPHyQn0Q=
|
||||
|
@ -1271,9 +1283,11 @@ modernc.org/sqlite v1.14.2 h1:ohsW2+e+Qe2To1W6GNezzKGwjXwSax6R+CrhRxVaFbE=
|
|||
modernc.org/sqlite v1.14.2/go.mod h1:yqfn85u8wVOE6ub5UT8VI9JjhrwBUUCNyTACN0h6Sx8=
|
||||
modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
|
||||
modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
|
||||
modernc.org/tcl v1.8.13 h1:V0sTNBw0Re86PvXZxuCub3oO9WrSTqALgrwNZNvLFGw=
|
||||
modernc.org/tcl v1.8.13/go.mod h1:V+q/Ef0IJaNUSECieLU4o+8IScapxnMyFV6i/7uQlAY=
|
||||
modernc.org/token v1.0.0 h1:a0jaWiNMDhDUtqOj09wvjWWAqd3q7WpBulmL9H2egsk=
|
||||
modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
modernc.org/z v1.2.19 h1:BGyRFWhDVn5LFS5OcX4Yd/MlpRTOc7hOPTdcIpCiUao=
|
||||
modernc.org/z v1.2.19/go.mod h1:+ZpP0pc4zz97eukOzW3xagV/lS82IpPN9NGG5pNF9vY=
|
||||
rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
|
||||
rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
|
||||
|
|
|
@ -52,6 +52,8 @@ backend:
|
|||
other: "Comment are not allowed to edit."
|
||||
not_found:
|
||||
other: "Comment not found."
|
||||
cannot_edit_after_deadline:
|
||||
other: "The comment time has been too long to modify."
|
||||
email:
|
||||
duplicate:
|
||||
other: "Email already exists."
|
||||
|
@ -508,6 +510,8 @@ ui:
|
|||
label: Revision
|
||||
answer:
|
||||
label: Answer
|
||||
feedback:
|
||||
characters: content must be at least 6 characters in length.
|
||||
edit_summary:
|
||||
label: Edit Summary
|
||||
placeholder: >-
|
||||
|
@ -638,7 +642,7 @@ ui:
|
|||
msg:
|
||||
empty: Email cannot be empty.
|
||||
change_email:
|
||||
page_title: Welcome to Answer
|
||||
page_title: Welcome to {{site_name}}
|
||||
btn_cancel: Cancel
|
||||
btn_update: Update email address
|
||||
send_success: >-
|
||||
|
@ -772,6 +776,7 @@ ui:
|
|||
<p>Are you sure you want to add another answer?</p><p>You could use the
|
||||
edit link to refine and improve your existing answer, instead.</p>
|
||||
empty: Answer cannot be empty.
|
||||
characters: content must be at least 6 characters in length.
|
||||
reopen:
|
||||
title: Reopen this post
|
||||
content: Are you sure you want to reopen?
|
||||
|
@ -838,7 +843,7 @@ ui:
|
|||
modal_confirm:
|
||||
title: Error...
|
||||
account_result:
|
||||
page_title: Welcome to Answer
|
||||
page_title: Welcome to {{site_name}}
|
||||
success: Your new account is confirmed; you will be redirected to the home page.
|
||||
link: Continue to homepage
|
||||
invalid: >-
|
||||
|
@ -993,8 +998,12 @@ ui:
|
|||
database tables first.
|
||||
db_failed: Database connection failed
|
||||
db_failed_desc: >-
|
||||
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host's database server is down.
|
||||
|
||||
This either means that the database information in your <1>config.yaml</1> file is incorrect or that contact with the database server could not be established. This could mean your host’s database server is down.
|
||||
counts:
|
||||
views: views
|
||||
votes: votes
|
||||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package constant
|
||||
|
||||
import "time"
|
||||
|
||||
const (
|
||||
CommentEditDeadline = time.Minute * 5
|
||||
)
|
|
@ -7,10 +7,10 @@ import (
|
|||
"github.com/answerdev/answer/pkg/dir"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
_ "github.com/lib/pq"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/segmentfault/pacman/cache"
|
||||
"github.com/segmentfault/pacman/contrib/cache/memory"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
_ "modernc.org/sqlite"
|
||||
"xorm.io/core"
|
||||
"xorm.io/xorm"
|
||||
ormlog "xorm.io/xorm/log"
|
||||
|
@ -38,11 +38,13 @@ func NewDB(debug bool, dataConf *Database) (*xorm.Engine, error) {
|
|||
dataConf.Driver = string(schemas.MYSQL)
|
||||
}
|
||||
if dataConf.Driver == string(schemas.SQLITE) {
|
||||
dataConf.Driver = "sqlite"
|
||||
dbFileDir := filepath.Dir(dataConf.Connection)
|
||||
log.Debugf("try to create database directory %s", dbFileDir)
|
||||
if err := dir.CreateDirIfNotExist(dbFileDir); err != nil {
|
||||
log.Errorf("create database dir failed: %s", err)
|
||||
}
|
||||
dataConf.MaxOpenConn = 1
|
||||
}
|
||||
engine, err := xorm.NewEngine(dataConf.Driver, dataConf.Connection)
|
||||
if err != nil {
|
||||
|
|
|
@ -16,6 +16,7 @@ const (
|
|||
const (
|
||||
EmailOrPasswordWrong = "error.object.email_or_password_incorrect"
|
||||
CommentNotFound = "error.comment.not_found"
|
||||
CommentCannotEditAfterDeadline = "error.comment.cannot_edit_after_deadline"
|
||||
QuestionNotFound = "error.question.not_found"
|
||||
QuestionCannotDeleted = "error.question.cannot_deleted"
|
||||
QuestionCannotClose = "error.question.cannot_close"
|
||||
|
|
|
@ -97,8 +97,29 @@ func getTran(lo locales.Translator) ut.Translator {
|
|||
return tran
|
||||
}
|
||||
|
||||
func NotBlank(fl validator.FieldLevel) (res bool) {
|
||||
field := fl.Field()
|
||||
switch field.Kind() {
|
||||
case reflect.String:
|
||||
trimSpace := strings.TrimSpace(field.String())
|
||||
res := len(trimSpace) > 0
|
||||
if !res {
|
||||
field.SetString(trimSpace)
|
||||
}
|
||||
return true
|
||||
case reflect.Chan, reflect.Map, reflect.Slice, reflect.Array:
|
||||
return field.Len() > 0
|
||||
case reflect.Ptr, reflect.Interface, reflect.Func:
|
||||
return !field.IsNil()
|
||||
default:
|
||||
return field.IsValid() && field.Interface() != reflect.Zero(field.Type()).Interface()
|
||||
}
|
||||
}
|
||||
|
||||
func createDefaultValidator(la i18n.Language) *validator.Validate {
|
||||
validate := validator.New()
|
||||
// _ = validate.RegisterValidation("notblank", validators.NotBlank)
|
||||
_ = validate.RegisterValidation("notblank", NotBlank)
|
||||
validate.RegisterTagNameFunc(func(fld reflect.StructField) (res string) {
|
||||
defer func() {
|
||||
if len(res) > 0 {
|
||||
|
|
|
@ -111,6 +111,7 @@ func (cc *CommentController) UpdateComment(ctx *gin.Context) {
|
|||
}
|
||||
|
||||
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
|
||||
req.IsAdmin = middleware.GetIsAdminFromContext(ctx)
|
||||
can, err := cc.rankService.CheckOperationPermission(ctx, req.UserID, permission.CommentEdit, req.CommentID)
|
||||
if err != nil {
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
|
|
|
@ -139,6 +139,9 @@ func (tc *TemplateController) QuestionList(ctx *gin.Context) {
|
|||
}
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions", siteInfo.General.SiteUrl)
|
||||
if page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/questions?page=%d", siteInfo.General.SiteUrl, page)
|
||||
}
|
||||
|
||||
UrlUseTitle := false
|
||||
if siteInfo.SiteSeo.PermaLink == schema.PermaLinkQuestionIDAndTitle {
|
||||
|
@ -327,6 +330,9 @@ func (tc *TemplateController) TagList(ctx *gin.Context) {
|
|||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags", siteInfo.General.SiteUrl)
|
||||
if req.Page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags?page=%d", siteInfo.General.SiteUrl, req.Page)
|
||||
}
|
||||
siteInfo.Title = fmt.Sprintf("%s - %s", "Tags", siteInfo.General.Name)
|
||||
tc.html(ctx, http.StatusOK, "tags.html", siteInfo, gin.H{
|
||||
"page": page,
|
||||
|
@ -353,6 +359,9 @@ func (tc *TemplateController) TagInfo(ctx *gin.Context) {
|
|||
|
||||
siteInfo := tc.SiteInfo(ctx)
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s", siteInfo.General.SiteUrl, tag)
|
||||
if req.Page > 1 {
|
||||
siteInfo.Canonical = fmt.Sprintf("%s/tags/%s?page=%d", siteInfo.General.SiteUrl, tag, req.Page)
|
||||
}
|
||||
siteInfo.Description = htmltext.FetchExcerpt(taginifo.ParsedText, "...", 240)
|
||||
if len(taginifo.ParsedText) == 0 {
|
||||
siteInfo.Description = "The tag has no description."
|
||||
|
@ -437,6 +446,7 @@ func (tc *TemplateController) html(ctx *gin.Context, code int, tpl string, siteI
|
|||
data["HeadCode"] = siteInfo.CustomCssHtml.CustomHead
|
||||
data["HeaderCode"] = siteInfo.CustomCssHtml.CustomHeader
|
||||
data["FooterCode"] = siteInfo.CustomCssHtml.CustomFooter
|
||||
data["Version"] = constant.Version
|
||||
_, ok := data["path"]
|
||||
if !ok {
|
||||
data["path"] = ""
|
||||
|
|
|
@ -157,8 +157,8 @@ func (uc *UserController) RetrievePassWord(ctx *gin.Context) {
|
|||
return
|
||||
}
|
||||
_, _ = uc.actionService.ActionRecordAdd(ctx, schema.ActionRecordTypeFindPass, ctx.ClientIP())
|
||||
code, err := uc.userService.RetrievePassWord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, code)
|
||||
_, err := uc.userService.RetrievePassWord(ctx, req)
|
||||
handler.HandleResponse(ctx, err, nil)
|
||||
}
|
||||
|
||||
// UseRePassWord godoc
|
||||
|
|
|
@ -5,6 +5,10 @@ import (
|
|||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/xorm/schemas"
|
||||
)
|
||||
|
||||
|
@ -82,6 +86,18 @@ type InitBaseInfoReq struct {
|
|||
AdminEmail string `validate:"required,email,gt=0,lte=500" json:"email"`
|
||||
}
|
||||
|
||||
func (r *InitBaseInfoReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if checker.IsInvalidUsername(r.AdminName) {
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "name",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
}
|
||||
errFields = append(errFields, errField)
|
||||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *InitBaseInfoReq) FormatSiteUrl() {
|
||||
parsedUrl, err := url.Parse(r.SiteURL)
|
||||
if err != nil {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
||||
// collectionRepo collection repository
|
||||
|
@ -29,15 +30,28 @@ func NewCollectionRepo(data *data.Data, uniqueIDRepo unique.UniqueIDRepo) collec
|
|||
|
||||
// AddCollection add collection
|
||||
func (cr *collectionRepo) AddCollection(ctx context.Context, collection *entity.Collection) (err error) {
|
||||
id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
|
||||
if err == nil {
|
||||
collection.ID = id
|
||||
_, err = cr.data.DB.Insert(collection)
|
||||
_, err = cr.data.DB.Transaction(func(session *xorm.Session) (result any, err error) {
|
||||
var has bool
|
||||
dbcollection := &entity.Collection{}
|
||||
result = nil
|
||||
has, err = session.Where("user_id = ? and object_id = ?", collection.UserID, collection.ObjectID).Get(dbcollection)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
return
|
||||
}
|
||||
}
|
||||
return nil
|
||||
if has {
|
||||
return
|
||||
}
|
||||
id, err := cr.uniqueIDRepo.GenUniqueIDStr(ctx, collection.TableName())
|
||||
if err == nil {
|
||||
collection.ID = id
|
||||
_, err = session.Insert(collection)
|
||||
if err != nil {
|
||||
return result, errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
}
|
||||
}
|
||||
return
|
||||
})
|
||||
return err
|
||||
}
|
||||
|
||||
// RemoveCollection delete collection
|
||||
|
|
|
@ -33,7 +33,7 @@ var (
|
|||
"`user_id`",
|
||||
"`vote_count`",
|
||||
"`answer_count`",
|
||||
"0 as `accepted`",
|
||||
"CASE WHEN `accepted_answer_id` > 0 THEN 2 ELSE 0 END as `accepted`",
|
||||
"`question`.`status` as `status`",
|
||||
"`post_update_time`",
|
||||
}
|
||||
|
|
|
@ -20,10 +20,10 @@ const (
|
|||
)
|
||||
|
||||
type AnswerAddReq struct {
|
||||
QuestionID string `json:"question_id" ` // question_id
|
||||
Content string `json:"content" ` // content
|
||||
HTML string `json:"html" ` // html
|
||||
UserID string `json:"-" ` // user_id
|
||||
QuestionID string `json:"question_id"`
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
HTML string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
}
|
||||
|
||||
func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
|
@ -32,13 +32,13 @@ func (req *AnswerAddReq) Check() (errFields []*validator.FormErrorField, err err
|
|||
}
|
||||
|
||||
type AnswerUpdateReq struct {
|
||||
ID string `json:"id"` // id
|
||||
QuestionID string `json:"question_id" ` // question_id
|
||||
UserID string `json:"-" ` // user_id
|
||||
Title string `json:"title" ` // title
|
||||
Content string `json:"content"` // content
|
||||
HTML string `json:"html" ` // html
|
||||
EditSummary string `validate:"omitempty" json:"edit_summary"` // edit_summary
|
||||
ID string `json:"id"`
|
||||
QuestionID string `json:"question_id"`
|
||||
Title string `json:"title"`
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
EditSummary string `validate:"omitempty" json:"edit_summary"`
|
||||
HTML string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
NoNeedReview bool `json:"-"`
|
||||
// whether user can edit it
|
||||
CanEdit bool `json:"-"`
|
||||
|
|
|
@ -14,9 +14,9 @@ type AddCommentReq struct {
|
|||
// reply comment id
|
||||
ReplyCommentID string `validate:"omitempty" json:"reply_comment_id"`
|
||||
// original comment content
|
||||
OriginalText string `validate:"required" json:"original_text"`
|
||||
OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"`
|
||||
// parsed comment content
|
||||
ParsedText string `validate:"required" json:"parsed_text"`
|
||||
ParsedText string `json:"-"`
|
||||
// @ user id list
|
||||
MentionUsernameList []string `validate:"omitempty" json:"mention_username_list"`
|
||||
// user id
|
||||
|
@ -47,11 +47,12 @@ type UpdateCommentReq struct {
|
|||
// comment id
|
||||
CommentID string `validate:"required" json:"comment_id"`
|
||||
// original comment content
|
||||
OriginalText string `validate:"omitempty" json:"original_text"`
|
||||
OriginalText string `validate:"required,notblank,gte=2,lte=600" json:"original_text"`
|
||||
// parsed comment content
|
||||
ParsedText string `validate:"omitempty" json:"parsed_text"`
|
||||
ParsedText string `json:"-"`
|
||||
// user id
|
||||
UserID string `json:"-"`
|
||||
UserID string `json:"-"`
|
||||
IsAdmin bool `json:"-"`
|
||||
}
|
||||
|
||||
func (req *UpdateCommentReq) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
|
|
|
@ -41,11 +41,11 @@ type ReopenQuestionReq struct {
|
|||
|
||||
type QuestionAdd struct {
|
||||
// question title
|
||||
Title string `validate:"required,gte=6,lte=150" json:"title"`
|
||||
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
|
||||
// content
|
||||
Content string `validate:"required,gte=6,lte=65535" json:"content"`
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
// html
|
||||
HTML string `validate:"required,gte=6,lte=65535" json:"html"`
|
||||
HTML string `json:"-"`
|
||||
// tags
|
||||
Tags []*TagItem `validate:"required,dive" json:"tags"`
|
||||
// user id
|
||||
|
@ -90,11 +90,11 @@ type QuestionUpdate struct {
|
|||
// question id
|
||||
ID string `validate:"required" json:"id"`
|
||||
// question title
|
||||
Title string `validate:"required,gte=6,lte=150" json:"title"`
|
||||
Title string `validate:"required,notblank,gte=6,lte=150" json:"title"`
|
||||
// content
|
||||
Content string `validate:"required,gte=6,lte=65535" json:"content"`
|
||||
Content string `validate:"required,notblank,gte=6,lte=65535" json:"content"`
|
||||
// html
|
||||
HTML string `validate:"required,gte=6,lte=65535" json:"html"`
|
||||
HTML string `json:"-"`
|
||||
// tags
|
||||
Tags []*TagItem `validate:"required,dive" json:"tags"`
|
||||
// edit summary
|
||||
|
@ -219,7 +219,7 @@ type UserQuestionInfo struct {
|
|||
ViewCount int `json:"view_count"`
|
||||
AnswerCount int `json:"answer_count"`
|
||||
CollectionCount int `json:"collection_count"`
|
||||
CreateTime int `json:"create_time"`
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
AcceptedAnswerID string `json:"accepted_answer_id"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
|
@ -253,6 +253,7 @@ const (
|
|||
|
||||
type QuestionPageResp struct {
|
||||
ID string `json:"id" `
|
||||
CreatedAt int64 `json:"created_at"`
|
||||
Title string `json:"title"`
|
||||
UrlTitle string `json:"url_title"`
|
||||
Description string `json:"description"`
|
||||
|
|
|
@ -144,7 +144,7 @@ type TagItem struct {
|
|||
// original text
|
||||
OriginalText string `validate:"omitempty" json:"original_text"`
|
||||
// parsed text
|
||||
ParsedText string `validate:"omitempty" json:"parsed_text"`
|
||||
ParsedText string `json:"-"`
|
||||
}
|
||||
|
||||
// RemoveTagReq delete tag request
|
||||
|
@ -166,7 +166,7 @@ type UpdateTagReq struct {
|
|||
// original text
|
||||
OriginalText string `validate:"omitempty" json:"original_text"`
|
||||
// parsed text
|
||||
ParsedText string `validate:"omitempty" json:"parsed_text"`
|
||||
ParsedText string `json:"-"`
|
||||
// edit summary
|
||||
EditSummary string `validate:"omitempty" json:"edit_summary"`
|
||||
// user id
|
||||
|
|
|
@ -2,12 +2,12 @@ package schema
|
|||
|
||||
import (
|
||||
"encoding/json"
|
||||
"regexp"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/base/validator"
|
||||
"github.com/answerdev/answer/internal/entity"
|
||||
"github.com/answerdev/answer/pkg/checker"
|
||||
"github.com/answerdev/answer/pkg/converter"
|
||||
"github.com/jinzhu/copier"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
)
|
||||
|
@ -283,7 +283,7 @@ type UpdateInfoRequest struct {
|
|||
// bio
|
||||
Bio string `validate:"omitempty,gt=0,lte=4096" json:"bio"`
|
||||
// bio
|
||||
BioHTML string `validate:"omitempty,gt=0,lte=4096" json:"bio_html"`
|
||||
BioHTML string `json:"-"`
|
||||
// website
|
||||
Website string `validate:"omitempty,gt=0,lte=500" json:"website"`
|
||||
// location
|
||||
|
@ -298,12 +298,9 @@ type AvatarInfo struct {
|
|||
Custom string `validate:"omitempty,gt=0,lte=200" json:"custom"`
|
||||
}
|
||||
|
||||
func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(u.Username) > 0 {
|
||||
errFields := make([]*validator.FormErrorField, 0)
|
||||
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
match := re.MatchString(u.Username)
|
||||
if !match {
|
||||
func (req *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err error) {
|
||||
if len(req.Username) > 0 {
|
||||
if checker.IsInvalidUsername(req.Username) {
|
||||
errField := &validator.FormErrorField{
|
||||
ErrorField: "username",
|
||||
ErrorMsg: reason.UsernameInvalid,
|
||||
|
@ -312,6 +309,7 @@ func (u *UpdateInfoRequest) Check() (errFields []*validator.FormErrorField, err
|
|||
return errFields, errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
}
|
||||
req.BioHTML = converter.Markdown2HTML(req.Bio)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -121,33 +121,14 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
return nil, err
|
||||
}
|
||||
|
||||
if objInfo.ObjectType == constant.QuestionObjectType {
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
|
||||
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
|
||||
} else if objInfo.ObjectType == constant.AnswerObjectType {
|
||||
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
|
||||
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
|
||||
}
|
||||
if len(req.MentionUsernameList) > 0 {
|
||||
cs.notificationMention(ctx, req.MentionUsernameList, comment.ID, req.UserID)
|
||||
}
|
||||
|
||||
resp = &schema.GetCommentResp{}
|
||||
resp.SetFromComment(comment)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
|
||||
time.Now(), req.CanEdit, req.CanDelete)
|
||||
|
||||
// get reply user info
|
||||
if len(resp.ReplyUserID) > 0 {
|
||||
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
resp.ReplyUsername = replyUser.Username
|
||||
resp.ReplyUserDisplayName = replyUser.DisplayName
|
||||
resp.ReplyUserStatus = replyUser.Status
|
||||
}
|
||||
cs.notificationCommentReply(ctx, replyUser.ID, objInfo.QuestionID, req.UserID)
|
||||
commentResp, err := cs.addCommentNotification(ctx, req, resp, comment, objInfo)
|
||||
if err != nil {
|
||||
return commentResp, err
|
||||
}
|
||||
|
||||
// get user info
|
||||
|
@ -178,6 +159,50 @@ func (cs *CommentService) AddComment(ctx context.Context, req *schema.AddComment
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
func (cs *CommentService) addCommentNotification(
|
||||
ctx context.Context, req *schema.AddCommentReq, resp *schema.GetCommentResp,
|
||||
comment *entity.Comment, objInfo *schema.SimpleObjectInfo) (*schema.GetCommentResp, error) {
|
||||
// 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
|
||||
if len(resp.ReplyUserID) > 0 && resp.ReplyUserID != req.UserID {
|
||||
replyUser, exist, err := cs.userCommon.GetUserBasicInfoByID(ctx, resp.ReplyUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if exist {
|
||||
resp.ReplyUsername = replyUser.Username
|
||||
resp.ReplyUserDisplayName = replyUser.DisplayName
|
||||
resp.ReplyUserStatus = replyUser.Status
|
||||
}
|
||||
cs.notificationCommentReply(ctx, replyUser.ID, comment.ID, req.UserID)
|
||||
alreadyNotifiedUserID[replyUser.ID] = true
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if len(req.MentionUsernameList) > 0 {
|
||||
alreadyNotifiedUserIDs := cs.notificationMention(
|
||||
ctx, req.MentionUsernameList, comment.ID, req.UserID, alreadyNotifiedUserID)
|
||||
for _, userID := range alreadyNotifiedUserIDs {
|
||||
alreadyNotifiedUserID[userID] = true
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
if objInfo.ObjectType == constant.QuestionObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
|
||||
cs.notificationQuestionComment(ctx, objInfo.ObjectCreatorUserID,
|
||||
objInfo.QuestionID, objInfo.Title, comment.ID, req.UserID, comment.OriginalText)
|
||||
} else if objInfo.ObjectType == constant.AnswerObjectType && !alreadyNotifiedUserID[objInfo.ObjectCreatorUserID] {
|
||||
cs.notificationAnswerComment(ctx, objInfo.QuestionID, objInfo.Title, objInfo.AnswerID,
|
||||
objInfo.ObjectCreatorUserID, comment.ID, req.UserID, comment.OriginalText)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// RemoveComment delete comment
|
||||
func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveCommentReq) (err error) {
|
||||
return cs.commentRepo.RemoveComment(ctx, req.CommentID)
|
||||
|
@ -185,6 +210,19 @@ func (cs *CommentService) RemoveComment(ctx context.Context, req *schema.RemoveC
|
|||
|
||||
// UpdateComment update comment
|
||||
func (cs *CommentService) UpdateComment(ctx context.Context, req *schema.UpdateCommentReq) (err error) {
|
||||
old, exist, err := cs.commentCommonRepo.GetComment(ctx, req.CommentID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
return errors.BadRequest(reason.CommentNotFound)
|
||||
}
|
||||
|
||||
// user can edit the comment that was posted by himself before deadline.
|
||||
if !req.IsAdmin && (time.Now().After(old.CreatedAt.Add(constant.CommentEditDeadline))) {
|
||||
return errors.BadRequest(reason.CommentCannotEditAfterDeadline)
|
||||
}
|
||||
|
||||
comment := &entity.Comment{}
|
||||
_ = copier.Copy(comment, req)
|
||||
comment.ID = req.CommentID
|
||||
|
@ -198,7 +236,7 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
|
|||
return
|
||||
}
|
||||
if !exist {
|
||||
return nil, errors.BadRequest(reason.UnknownError)
|
||||
return nil, errors.BadRequest(reason.CommentNotFound)
|
||||
}
|
||||
|
||||
resp = &schema.GetCommentResp{
|
||||
|
@ -243,7 +281,8 @@ func (cs *CommentService) GetComment(ctx context.Context, req *schema.GetComment
|
|||
// check if current user vote this comment
|
||||
resp.IsVote = cs.checkIsVote(ctx, req.UserID, resp.CommentID)
|
||||
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID, req.CanEdit, req.CanDelete)
|
||||
resp.MemberActions = permission.GetCommentPermission(ctx, req.UserID, resp.UserID,
|
||||
comment.CreatedAt, req.CanEdit, req.CanDelete)
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
|
@ -339,7 +378,7 @@ func (cs *CommentService) convertCommentEntity2Resp(ctx context.Context, req *sc
|
|||
commentResp.IsVote = cs.checkIsVote(ctx, req.UserID, commentResp.CommentID)
|
||||
|
||||
commentResp.MemberActions = permission.GetCommentPermission(ctx,
|
||||
req.UserID, commentResp.UserID, req.CanEdit, req.CanDelete)
|
||||
req.UserID, commentResp.UserID, comment.CreatedAt, req.CanEdit, req.CanDelete)
|
||||
return commentResp, nil
|
||||
}
|
||||
|
||||
|
@ -521,14 +560,16 @@ func (cs *CommentService) notificationCommentReply(ctx context.Context, replyUse
|
|||
notice_queue.AddNotification(msg)
|
||||
}
|
||||
|
||||
func (cs *CommentService) notificationMention(ctx context.Context, mentionUsernameList []string, commentID, commentUserID string) {
|
||||
func (cs *CommentService) 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 {
|
||||
if exist && !alreadyNotifiedUserID[userInfo.ID] {
|
||||
msg := &schema.NotificationMsg{
|
||||
ReceiverUserID: userInfo.ID,
|
||||
TriggerUserID: commentUserID,
|
||||
|
@ -538,6 +579,8 @@ func (cs *CommentService) notificationMention(ctx context.Context, mentionUserna
|
|||
msg.ObjectType = constant.CommentObjectType
|
||||
msg.NotificationAction = constant.MentionYou
|
||||
notice_queue.AddNotification(msg)
|
||||
alreadyNotifiedUserIDs = append(alreadyNotifiedUserIDs, userInfo.ID)
|
||||
}
|
||||
}
|
||||
return alreadyNotifiedUserIDs
|
||||
}
|
||||
|
|
|
@ -2,13 +2,15 @@ package permission
|
|||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
)
|
||||
|
||||
// GetCommentPermission get comment permission
|
||||
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string, canEdit, canDelete bool) (
|
||||
actions []*schema.PermissionMemberAction) {
|
||||
func GetCommentPermission(ctx context.Context, userID string, creatorUserID string,
|
||||
createdAt time.Time, canEdit, canDelete bool) (actions []*schema.PermissionMemberAction) {
|
||||
actions = make([]*schema.PermissionMemberAction, 0)
|
||||
if len(userID) > 0 {
|
||||
actions = append(actions, &schema.PermissionMemberAction{
|
||||
|
@ -17,7 +19,8 @@ func GetCommentPermission(ctx context.Context, userID string, creatorUserID stri
|
|||
Type: "reason",
|
||||
})
|
||||
}
|
||||
if canEdit || userID == creatorUserID {
|
||||
deadline := createdAt.Add(constant.CommentEditDeadline)
|
||||
if canEdit || (userID == creatorUserID && time.Now().Before(deadline)) {
|
||||
actions = append(actions, &schema.PermissionMemberAction{
|
||||
Action: "edit",
|
||||
Name: "Edit",
|
||||
|
|
|
@ -254,6 +254,7 @@ func (qs *QuestionCommon) FormatQuestionsPage(
|
|||
for _, questionInfo := range questionList {
|
||||
t := &schema.QuestionPageResp{
|
||||
ID: questionInfo.ID,
|
||||
CreatedAt: questionInfo.CreatedAt.Unix(),
|
||||
Title: questionInfo.Title,
|
||||
UrlTitle: htmltext.UrlTitle(questionInfo.Title),
|
||||
Description: htmltext.FetchExcerpt(questionInfo.ParsedText, "...", 240),
|
||||
|
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/Chain-Zhang/pinyin"
|
||||
|
@ -120,9 +119,7 @@ func (us *UserCommon) MakeUsername(ctx context.Context, displayName string) (use
|
|||
username = strings.ToLower(username)
|
||||
suffix := ""
|
||||
|
||||
re := regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
match := re.MatchString(username)
|
||||
if !match {
|
||||
if checker.IsInvalidUsername(username) {
|
||||
return "", errors.BadRequest(reason.UsernameInvalid)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package checker
|
||||
|
||||
import "regexp"
|
||||
|
||||
var (
|
||||
usernameReg = regexp.MustCompile(`^[a-z0-9._-]{4,30}$`)
|
||||
)
|
||||
|
||||
func IsInvalidUsername(username string) bool {
|
||||
return !usernameReg.MatchString(username)
|
||||
}
|
|
@ -3,6 +3,7 @@ package converter
|
|||
import (
|
||||
"bytes"
|
||||
|
||||
"github.com/asaskevich/govalidator"
|
||||
"github.com/microcosm-cc/bluemonday"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
"github.com/yuin/goldmark"
|
||||
|
@ -10,6 +11,7 @@ import (
|
|||
"github.com/yuin/goldmark/extension"
|
||||
"github.com/yuin/goldmark/parser"
|
||||
"github.com/yuin/goldmark/renderer"
|
||||
"github.com/yuin/goldmark/renderer/html"
|
||||
goldmarkHTML "github.com/yuin/goldmark/renderer/html"
|
||||
"github.com/yuin/goldmark/util"
|
||||
)
|
||||
|
@ -54,6 +56,7 @@ type DangerousHTMLRenderer struct {
|
|||
func (r *DangerousHTMLRenderer) RegisterFuncs(reg renderer.NodeRendererFuncRegisterer) {
|
||||
reg.Register(ast.KindHTMLBlock, r.renderHTMLBlock)
|
||||
reg.Register(ast.KindRawHTML, r.renderRawHTML)
|
||||
reg.Register(ast.KindLink, r.renderLink)
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderRawHTML(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
|
@ -85,3 +88,30 @@ func (r *DangerousHTMLRenderer) renderHTMLBlock(w util.BufWriter, source []byte,
|
|||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderLink(w util.BufWriter, source []byte, node ast.Node, entering bool) (ast.WalkStatus, error) {
|
||||
n := node.(*ast.Link)
|
||||
if entering && r.renderLinkIsUrl(string(n.Destination)) {
|
||||
_, _ = w.WriteString("<a href=\"")
|
||||
if r.Unsafe || !html.IsDangerousURL(n.Destination) {
|
||||
_, _ = w.Write(util.EscapeHTML(util.URLEscape(n.Destination, true)))
|
||||
}
|
||||
_ = w.WriteByte('"')
|
||||
if n.Title != nil {
|
||||
_, _ = w.WriteString(` title="`)
|
||||
r.Writer.Write(w, n.Title)
|
||||
_ = w.WriteByte('"')
|
||||
}
|
||||
if n.Attributes() != nil {
|
||||
html.RenderAttributes(w, n, html.LinkAttributeFilter)
|
||||
}
|
||||
_ = w.WriteByte('>')
|
||||
} else {
|
||||
_, _ = w.WriteString("</a>")
|
||||
}
|
||||
return ast.WalkContinue, nil
|
||||
}
|
||||
|
||||
func (r *DangerousHTMLRenderer) renderLinkIsUrl(verifyUrl string) bool {
|
||||
return govalidator.IsURL(verifyUrl)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,11 @@
|
|||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
/build/*/*/*
|
||||
/build/*.json
|
||||
/build/*.html
|
||||
/build/*.txt
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 4.2 KiB |
|
@ -1 +0,0 @@
|
|||
<!doctype html><html><head><meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/><meta name="theme-color" content="#000000"/><link rel="manifest" href="/manifest.json"/><script defer="defer" src="/static/js/main.cb9bf782.js"></script><link href="/static/css/main.b8d8739f.css" rel="stylesheet"></head><body><noscript>You need to enable JavaScript to run this app.</noscript><div id="root"><div id="spin-mask"><noscript><style>#spin-mask{display:none!important}</style></noscript><style>@keyframes _doc-spin{to{transform:rotate(360deg)}}#spin-mask{position:fixed;top:0;right:0;bottom:0;left:0;background-color:#fff;z-index:9999}#spin-container{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%)}#spin-container .spinner{box-sizing:border-box;display:inline-block;width:2rem;height:2rem;vertical-align:-.125em;border:.25rem solid currentColor;border-right-color:transparent;color:rgba(108,117,125,.75);border-radius:50%;animation:.75s linear infinite _doc-spin}</style><div id="spin-container"><div class="spinner"></div></div></div></div></body></html>
|
|
@ -22,7 +22,9 @@
|
|||
"copy-to-clipboard": "^3.3.2",
|
||||
"dayjs": "^1.11.5",
|
||||
"diff": "^5.1.0",
|
||||
"dompurify": "^2.4.3",
|
||||
"emoji-regex": "^10.2.1",
|
||||
"html-react-parser": "^3.0.8",
|
||||
"i18next": "^21.9.0",
|
||||
"katex": "^0.16.2",
|
||||
"lodash": "^4.17.21",
|
||||
|
@ -51,6 +53,7 @@
|
|||
"@testing-library/react": "^13.3.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"@types/color": "^3.0.3",
|
||||
"@types/dompurify": "^2.4.0",
|
||||
"@types/jest": "^27.5.2",
|
||||
"@types/lodash": "^4.14.184",
|
||||
"@types/marked": "^4.0.6",
|
||||
|
|
1412
ui/pnpm-lock.yaml
1412
ui/pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
|
@ -4,6 +4,7 @@
|
|||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<meta name="generator" content="Answer %AnswerVersion% - https://github.com/answerdev/answer">
|
||||
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -56,7 +56,7 @@ export interface QuestionParams {
|
|||
title: string;
|
||||
url_title?: string;
|
||||
content: string;
|
||||
html: string;
|
||||
html?: string;
|
||||
tags: Tag[];
|
||||
}
|
||||
|
||||
|
@ -207,7 +207,7 @@ export interface AnswerItem {
|
|||
|
||||
export interface PostAnswerReq {
|
||||
content: string;
|
||||
html: string;
|
||||
html?: string;
|
||||
question_id: string;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { useState, useEffect, memo } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Button, Form } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
@ -7,7 +7,7 @@ import classNames from 'classnames';
|
|||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
|
||||
const Form = ({
|
||||
const Index = ({
|
||||
className = '',
|
||||
value: initialValue = '',
|
||||
onSendReply,
|
||||
|
@ -18,7 +18,7 @@ const Form = ({
|
|||
const [value, setValue] = useState('');
|
||||
const pageUsers = usePageUsers();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
|
||||
|
||||
const [validationErrorMsg, setValidationErrorMsg] = useState('');
|
||||
useEffect(() => {
|
||||
if (!initialValue) {
|
||||
return;
|
||||
|
@ -32,6 +32,13 @@ const Form = ({
|
|||
const handleSelected = (val) => {
|
||||
setValue(val);
|
||||
};
|
||||
const handleSendReply = () => {
|
||||
onSendReply(value).catch((ex) => {
|
||||
if (ex.isError) {
|
||||
setValidationErrorMsg(ex.msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
|
@ -39,17 +46,27 @@ const Form = ({
|
|||
className,
|
||||
)}>
|
||||
<div>
|
||||
<Mentions pageUsers={pageUsers.getUsers()} onSelected={handleSelected}>
|
||||
<TextArea size="sm" value={value} onChange={handleChange} />
|
||||
</Mentions>
|
||||
<div className="form-text">{t(`tip_${mode}`)}</div>
|
||||
<div
|
||||
className={classNames('custom-form-control', {
|
||||
'is-invalid': validationErrorMsg,
|
||||
})}>
|
||||
<Mentions
|
||||
pageUsers={pageUsers.getUsers()}
|
||||
onSelected={handleSelected}>
|
||||
<TextArea size="sm" value={value} onChange={handleChange} />
|
||||
</Mentions>
|
||||
<div className="form-text">{t(`tip_${mode}`)}</div>
|
||||
</div>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{validationErrorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</div>
|
||||
{type === 'edit' ? (
|
||||
<div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0">
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-nowrap "
|
||||
onClick={() => onSendReply(value)}>
|
||||
onClick={() => handleSendReply()}>
|
||||
{t('btn_save_edits')}
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -64,7 +81,7 @@ const Form = ({
|
|||
<Button
|
||||
size="sm"
|
||||
className="text-nowrap ms-0 ms-md-2 mt-2 mt-md-0"
|
||||
onClick={() => onSendReply(value)}>
|
||||
onClick={() => handleSendReply()}>
|
||||
{t('btn_add_comment')}
|
||||
</Button>
|
||||
)}
|
||||
|
@ -72,4 +89,4 @@ const Form = ({
|
|||
);
|
||||
};
|
||||
|
||||
export default memo(Form);
|
||||
export default memo(Index);
|
||||
|
|
|
@ -1,21 +1,30 @@
|
|||
import { useState, memo } from 'react';
|
||||
import { Button } from 'react-bootstrap';
|
||||
import { Button, Form } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { TextArea, Mentions } from '@/components';
|
||||
import { usePageUsers } from '@/hooks';
|
||||
|
||||
const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
||||
const Index = ({ userName, onSendReply, onCancel, mode }) => {
|
||||
const [value, setValue] = useState('');
|
||||
const pageUsers = usePageUsers();
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'comment' });
|
||||
|
||||
const [validationErrorMsg, setValidationErrorMsg] = useState('');
|
||||
const handleChange = (e) => {
|
||||
setValue(e.target.value);
|
||||
};
|
||||
const handleSelected = (val) => {
|
||||
setValue(val);
|
||||
};
|
||||
const handleSendReply = () => {
|
||||
onSendReply(value).catch((ex) => {
|
||||
if (ex.isError) {
|
||||
setValidationErrorMsg(ex.msg);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-2">
|
||||
|
@ -24,18 +33,26 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
|||
</div>
|
||||
<div className="d-flex mb-1 align-items-start flex-column flex-md-row">
|
||||
<div>
|
||||
<Mentions
|
||||
pageUsers={pageUsers.getUsers()}
|
||||
onSelected={handleSelected}>
|
||||
<TextArea size="sm" value={value} onChange={handleChange} />
|
||||
</Mentions>
|
||||
<div className="form-text">{t(`tip_${mode}`)}</div>
|
||||
<div
|
||||
className={classNames('custom-form-control', {
|
||||
'is-invalid': validationErrorMsg,
|
||||
})}>
|
||||
<Mentions
|
||||
pageUsers={pageUsers.getUsers()}
|
||||
onSelected={handleSelected}>
|
||||
<TextArea size="sm" value={value} onChange={handleChange} />
|
||||
</Mentions>
|
||||
<div className="form-text">{t(`tip_${mode}`)}</div>
|
||||
</div>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{validationErrorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</div>
|
||||
<div className="d-flex flex-row flex-md-column ms-0 ms-md-2 mt-2 mt-md-0">
|
||||
<Button
|
||||
size="sm"
|
||||
className="text-nowrap"
|
||||
onClick={() => onSendReply(value)}>
|
||||
onClick={() => handleSendReply()}>
|
||||
{t('btn_add_comment')}
|
||||
</Button>
|
||||
<Button
|
||||
|
@ -51,4 +68,4 @@ const Form = ({ userName, onSendReply, onCancel, mode }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default memo(Form);
|
||||
export default memo(Index);
|
||||
|
|
|
@ -10,7 +10,12 @@ import { marked } from 'marked';
|
|||
import * as Types from '@/common/interface';
|
||||
import { Modal } from '@/components';
|
||||
import { usePageUsers, useReportModal } from '@/hooks';
|
||||
import { matchedUsers, parseUserInfo, scrollTop, bgFadeOut } from '@/utils';
|
||||
import {
|
||||
matchedUsers,
|
||||
parseUserInfo,
|
||||
scrollToElementTop,
|
||||
bgFadeOut,
|
||||
} from '@/utils';
|
||||
import { tryNormalLogged } from '@/utils/guard';
|
||||
import {
|
||||
useQueryComments,
|
||||
|
@ -43,7 +48,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
const scrollCallback = useCallback((el, co) => {
|
||||
if (pageIndex === 0 && co.comment_id === commentId) {
|
||||
setTimeout(() => {
|
||||
scrollTop(el);
|
||||
scrollToElementTop(el);
|
||||
bgFadeOut(el);
|
||||
}, 100);
|
||||
}
|
||||
|
@ -102,13 +107,14 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
const handleSendReply = (item) => {
|
||||
const users = matchedUsers(item.value);
|
||||
const userNames = unionBy(users.map((user) => user.userName));
|
||||
const html = marked.parse(parseUserInfo(item.value));
|
||||
if (!item.value || !html) {
|
||||
return;
|
||||
}
|
||||
const commentMarkDown = parseUserInfo(item.value);
|
||||
const html = marked.parse(commentMarkDown);
|
||||
// if (!commentMarkDown || !html) {
|
||||
// return;
|
||||
// }
|
||||
const params = {
|
||||
object_id: objectId,
|
||||
original_text: item.value,
|
||||
original_text: commentMarkDown,
|
||||
mention_username_list: userNames,
|
||||
parsed_text: html,
|
||||
...(item.type === 'reply'
|
||||
|
@ -119,7 +125,7 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
};
|
||||
|
||||
if (item.type === 'edit') {
|
||||
updateComment({
|
||||
return updateComment({
|
||||
...params,
|
||||
comment_id: item.comment_id,
|
||||
}).then(() => {
|
||||
|
@ -134,30 +140,29 @@ const Comment = ({ objectId, mode, commentId }) => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
} else {
|
||||
addComment(params).then((res) => {
|
||||
if (item.type === 'reply') {
|
||||
const index = comments.findIndex(
|
||||
(comment) => comment.comment_id === item.comment_id,
|
||||
);
|
||||
comments[index].showReply = false;
|
||||
comments.splice(index + 1, 0, res);
|
||||
setComments([...comments]);
|
||||
} else {
|
||||
setComments([
|
||||
...comments.map((comment) => {
|
||||
if (comment.comment_id === item.comment_id) {
|
||||
comment.showReply = false;
|
||||
}
|
||||
return comment;
|
||||
}),
|
||||
res,
|
||||
]);
|
||||
}
|
||||
|
||||
setVisibleComment(false);
|
||||
});
|
||||
}
|
||||
return addComment(params).then((res) => {
|
||||
if (item.type === 'reply') {
|
||||
const index = comments.findIndex(
|
||||
(comment) => comment.comment_id === item.comment_id,
|
||||
);
|
||||
comments[index].showReply = false;
|
||||
comments.splice(index + 1, 0, res);
|
||||
setComments([...comments]);
|
||||
} else {
|
||||
setComments([
|
||||
...comments.map((comment) => {
|
||||
if (comment.comment_id === item.comment_id) {
|
||||
comment.showReply = false;
|
||||
}
|
||||
return comment;
|
||||
}),
|
||||
res,
|
||||
]);
|
||||
}
|
||||
|
||||
setVisibleComment(false);
|
||||
});
|
||||
};
|
||||
|
||||
const handleDelete = (id) => {
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
import { FC, memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import classname from 'classnames';
|
||||
|
||||
import { Icon } from '@/components';
|
||||
|
||||
interface Props {
|
||||
data: {
|
||||
votes: number;
|
||||
answers: number;
|
||||
views: number;
|
||||
};
|
||||
showVotes?: boolean;
|
||||
showAnswers?: boolean;
|
||||
showViews?: boolean;
|
||||
showAccepted?: boolean;
|
||||
isAccepted?: boolean;
|
||||
className?: string;
|
||||
}
|
||||
const Index: FC<Props> = ({
|
||||
data,
|
||||
showVotes = true,
|
||||
showAnswers = true,
|
||||
showViews = true,
|
||||
isAccepted = false,
|
||||
showAccepted = false,
|
||||
className = '',
|
||||
}) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'counts' });
|
||||
|
||||
return (
|
||||
<div className={classname('d-flex align-items-center', className)}>
|
||||
{showVotes && (
|
||||
<div className="d-flex align-items-center">
|
||||
<Icon name="hand-thumbs-up-fill me-1" />
|
||||
<span>
|
||||
{data.votes} {t('votes')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAccepted && (
|
||||
<div className="d-flex align-items-center ms-3 text-success">
|
||||
<Icon name="check-circle-fill me-1" />
|
||||
<span>{t('accepted')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showAnswers && (
|
||||
<div
|
||||
className={`d-flex align-items-center ms-3 ${
|
||||
isAccepted ? 'text-success' : ''
|
||||
}`}>
|
||||
{isAccepted ? (
|
||||
<Icon name="check-circle-fill me-1" />
|
||||
) : (
|
||||
<Icon name="chat-square-text-fill me-1" />
|
||||
)}
|
||||
<span>
|
||||
{data.answers} {t('answers')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{showViews && (
|
||||
<span className="summary-stat ms-3">
|
||||
<Icon name="eye-fill" />
|
||||
<em className="fst-normal ms-1">
|
||||
{data.views} {t('views')}
|
||||
</em>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -8,6 +8,7 @@ import {
|
|||
} from 'react';
|
||||
|
||||
import { markdownToHtml } from '@/services';
|
||||
import { htmlToReact } from '@/utils';
|
||||
|
||||
import { htmlRender } from './utils';
|
||||
|
||||
|
@ -38,6 +39,7 @@ const Index = ({ value }, ref) => {
|
|||
}
|
||||
|
||||
previewRef.current?.scrollTo(0, scrollTop);
|
||||
|
||||
htmlRender(previewRef.current);
|
||||
}, [html]);
|
||||
useImperativeHandle(ref, () => {
|
||||
|
@ -49,9 +51,9 @@ const Index = ({ value }, ref) => {
|
|||
return (
|
||||
<div
|
||||
ref={previewRef}
|
||||
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt"
|
||||
dangerouslySetInnerHTML={{ __html: html }}
|
||||
/>
|
||||
className="preview-wrap position-relative p-3 bg-light rounded text-break text-wrap mt-2 fmt">
|
||||
{htmlToReact(html)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -3,6 +3,8 @@ import { Pagination } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
import { useSearchParams, useNavigate, useLocation } from 'react-router-dom';
|
||||
|
||||
import { scrollToDocTop } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
currentPage: number;
|
||||
pageSize: number;
|
||||
|
@ -49,7 +51,7 @@ const PageItem = ({ page, currentPage, path }: PageItemProps) => {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
navigate(path);
|
||||
window.scrollTo(0, 0);
|
||||
scrollToDocTop();
|
||||
}}>
|
||||
{page}
|
||||
</Pagination.Item>
|
||||
|
@ -91,7 +93,7 @@ const Index: FC<Props> = ({
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(handleParams(currentPage - 1));
|
||||
window.scrollTo(0, 0);
|
||||
scrollToDocTop();
|
||||
}}>
|
||||
{t('prev')}
|
||||
</Pagination.Prev>
|
||||
|
@ -186,7 +188,7 @@ const Index: FC<Props> = ({
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
navigate(handleParams(currentPage + 1));
|
||||
window.scrollTo(0, 0);
|
||||
scrollToDocTop();
|
||||
}}>
|
||||
{t('next')}
|
||||
</Pagination.Next>
|
||||
|
|
|
@ -6,7 +6,6 @@ import { useTranslation } from 'react-i18next';
|
|||
import { pathFactory } from '@/router/pathFactory';
|
||||
import type * as Type from '@/common/interface';
|
||||
import {
|
||||
Icon,
|
||||
Tag,
|
||||
Pagination,
|
||||
FormatTime,
|
||||
|
@ -14,6 +13,7 @@ import {
|
|||
BaseUserCard,
|
||||
QueryGroup,
|
||||
QuestionListLoader,
|
||||
Counts,
|
||||
} from '@/components';
|
||||
import { useQuestionList } from '@/services';
|
||||
|
||||
|
@ -95,29 +95,15 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
preFix={t(li.operation_type)}
|
||||
/>
|
||||
</div>
|
||||
<div className="ms-0 ms-md-3 mt-2 mt-md-0">
|
||||
<span>
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
<em className="fst-normal ms-1">{li.vote_count}</em>
|
||||
</span>
|
||||
<span
|
||||
className={`ms-3 ${
|
||||
li.accepted_answer_id >= 1 ? 'text-success' : ''
|
||||
}`}>
|
||||
<Icon
|
||||
name={
|
||||
li.accepted_answer_id >= 1
|
||||
? 'check-circle-fill'
|
||||
: 'chat-square-text-fill'
|
||||
}
|
||||
/>
|
||||
<em className="fst-normal ms-1">{li.answer_count}</em>
|
||||
</span>
|
||||
<span className="summary-stat ms-3">
|
||||
<Icon name="eye-fill" />
|
||||
<em className="fst-normal ms-1">{li.view_count}</em>
|
||||
</span>
|
||||
</div>
|
||||
<Counts
|
||||
data={{
|
||||
votes: li.vote_count,
|
||||
answers: li.answer_count,
|
||||
views: li.view_count,
|
||||
}}
|
||||
isAccepted={li.accepted_answer_id >= 1}
|
||||
className="ms-0 ms-md-3 mt-2 mt-md-0"
|
||||
/>
|
||||
</div>
|
||||
<div className="question-tags m-n1">
|
||||
{Array.isArray(li.tags)
|
||||
|
@ -139,7 +125,7 @@ const QuestionList: FC<Props> = ({ source }) => {
|
|||
currentPage={curPage}
|
||||
totalSize={count}
|
||||
pageSize={pageSize}
|
||||
pathname="/questions"
|
||||
pathname={source === 'questions' ? '/questions' : ''}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -155,6 +155,7 @@ const TagSelector: FC<IProps> = ({
|
|||
};
|
||||
const handleKeyDown = (e) => {
|
||||
e.stopPropagation();
|
||||
|
||||
if (!tags) {
|
||||
return;
|
||||
}
|
||||
|
@ -166,13 +167,20 @@ const TagSelector: FC<IProps> = ({
|
|||
if (keyCode === 40 && currentIndex < tags.length - 1) {
|
||||
setCurrentIndex(currentIndex + 1);
|
||||
}
|
||||
if (
|
||||
keyCode === 13 &&
|
||||
currentIndex > -1 &&
|
||||
currentIndex <= tags.length - 1
|
||||
) {
|
||||
|
||||
if (keyCode === 13 && currentIndex > -1) {
|
||||
e.preventDefault();
|
||||
handleClick(tags[currentIndex]);
|
||||
|
||||
if (tags.length === 0) {
|
||||
tagModal.onShow(tag);
|
||||
return;
|
||||
}
|
||||
if (currentIndex <= tags.length - 1) {
|
||||
handleClick(tags[currentIndex]);
|
||||
if (currentIndex === tags.length - 1 && currentIndex > 0) {
|
||||
setCurrentIndex(currentIndex - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
return (
|
||||
|
|
|
@ -32,6 +32,7 @@ import CustomizeTheme from './CustomizeTheme';
|
|||
import PageTags from './PageTags';
|
||||
import QuestionListLoader from './QuestionListLoader';
|
||||
import TagsLoader from './TagsLoader';
|
||||
import Counts from './Counts';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -70,5 +71,6 @@ export {
|
|||
PageTags,
|
||||
QuestionListLoader,
|
||||
TagsLoader,
|
||||
Counts,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -143,21 +143,7 @@ const Ask = () => {
|
|||
const checkValidated = (): boolean => {
|
||||
const bol = true;
|
||||
const { title, content, tags, answer } = formData;
|
||||
if (!title.value) {
|
||||
// bol = false;
|
||||
// formData.title = {
|
||||
// value: '',
|
||||
// isInvalid: true,
|
||||
// errorMsg: t('form.fields.title.msg.empty'),
|
||||
// };
|
||||
} else if (Array.from(title.value).length > 150) {
|
||||
// bol = false;
|
||||
// formData.title = {
|
||||
// value: title.value,
|
||||
// isInvalid: true,
|
||||
// errorMsg: t('form.fields.title.msg.range'),
|
||||
// };
|
||||
} else {
|
||||
if (title.value && Array.from(title.value).length <= 150) {
|
||||
formData.title = {
|
||||
value: title.value,
|
||||
isInvalid: false,
|
||||
|
@ -165,14 +151,7 @@ const Ask = () => {
|
|||
};
|
||||
}
|
||||
|
||||
if (!content.value) {
|
||||
// bol = false;
|
||||
// formData.content = {
|
||||
// value: '',
|
||||
// isInvalid: true,
|
||||
// errorMsg: t('form.fields.body.msg.empty'),
|
||||
// };
|
||||
} else {
|
||||
if (content.value) {
|
||||
formData.content = {
|
||||
value: content.value,
|
||||
isInvalid: false,
|
||||
|
@ -180,29 +159,16 @@ const Ask = () => {
|
|||
};
|
||||
}
|
||||
|
||||
if (tags.value.length === 0) {
|
||||
// bol = false;
|
||||
// formData.tags = {
|
||||
// value: [],
|
||||
// isInvalid: true,
|
||||
// errorMsg: t('form.fields.tags.msg.empty'),
|
||||
// };
|
||||
} else {
|
||||
if (Array.isArray(tags.value) && tags.value.length > 0) {
|
||||
formData.tags = {
|
||||
value: tags.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
}
|
||||
|
||||
if (checked) {
|
||||
if (!answer.value) {
|
||||
// bol = false;
|
||||
// formData.answer = {
|
||||
// value: '',
|
||||
// isInvalid: true,
|
||||
// errorMsg: t('form.fields.answer.msg.empty'),
|
||||
// };
|
||||
} else {
|
||||
if (answer.value) {
|
||||
formData.answer = {
|
||||
value: answer.value,
|
||||
isInvalid: false,
|
||||
|
@ -227,7 +193,6 @@ const Ask = () => {
|
|||
const params: Type.QuestionParams = {
|
||||
title: formData.title.value,
|
||||
content: formData.content.value,
|
||||
html: editorRef.current.getHtml(),
|
||||
tags: formData.tags.value,
|
||||
};
|
||||
if (isEdit) {
|
||||
|
@ -261,7 +226,6 @@ const Ask = () => {
|
|||
postAnswer({
|
||||
question_id: id,
|
||||
content: formData.answer.value,
|
||||
html: editorRef2.current.getHtml(),
|
||||
})
|
||||
.then(() => {
|
||||
navigate(pathFactory.questionLanding(id, params.url_title));
|
||||
|
|
|
@ -12,7 +12,7 @@ import {
|
|||
FormatTime,
|
||||
htmlRender,
|
||||
} from '@/components';
|
||||
import { scrollTop, bgFadeOut } from '@/utils';
|
||||
import { scrollToElementTop, bgFadeOut } from '@/utils';
|
||||
import { AnswerItem } from '@/common/interface';
|
||||
import { acceptanceAnswer } from '@/services';
|
||||
|
||||
|
@ -60,7 +60,7 @@ const Index: FC<Props> = ({
|
|||
if (aid === data.id) {
|
||||
setTimeout(() => {
|
||||
const element = answerRef.current;
|
||||
scrollTop(element);
|
||||
scrollToElementTop(element);
|
||||
if (!searchParams.get('commentId')) {
|
||||
bgFadeOut(answerRef.current);
|
||||
}
|
||||
|
|
|
@ -8,7 +8,7 @@ import classNames from 'classnames';
|
|||
import { Editor, Modal, TextArea } from '@/components';
|
||||
import { FormDataType } from '@/common/interface';
|
||||
import { postAnswer } from '@/services';
|
||||
import { guard } from '@/utils';
|
||||
import { guard, handleFormError } from '@/utils';
|
||||
|
||||
interface Props {
|
||||
visible?: boolean;
|
||||
|
@ -35,35 +35,60 @@ const Index: FC<Props> = ({ visible = false, data, callback }) => {
|
|||
const [focusType, setFocusType] = useState('');
|
||||
const [editorFocusState, setEditorFocusState] = useState(false);
|
||||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { content } = formData;
|
||||
|
||||
if (!content.value || Array.from(content.value.trim()).length < 6) {
|
||||
bol = false;
|
||||
formData.content = {
|
||||
value: content.value,
|
||||
isInvalid: true,
|
||||
errorMsg: t('characters'),
|
||||
};
|
||||
} else {
|
||||
formData.content = {
|
||||
value: content.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
}
|
||||
|
||||
setFormData({
|
||||
...formData,
|
||||
});
|
||||
return bol;
|
||||
};
|
||||
|
||||
const handleSubmit = () => {
|
||||
if (!guard.tryNormalLogged(true)) {
|
||||
return;
|
||||
}
|
||||
if (!formData.content.value) {
|
||||
setFormData({
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: true,
|
||||
errorMsg: t('empty'),
|
||||
},
|
||||
});
|
||||
if (!checkValidated()) {
|
||||
return;
|
||||
}
|
||||
postAnswer({
|
||||
question_id: data?.qid,
|
||||
content: formData.content.value,
|
||||
html: marked.parse(formData.content.value),
|
||||
}).then((res) => {
|
||||
setShowEditor(false);
|
||||
setFormData({
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
})
|
||||
.then((res) => {
|
||||
setShowEditor(false);
|
||||
setFormData({
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
callback?.(res.info);
|
||||
})
|
||||
.catch((ex) => {
|
||||
if (ex.isError) {
|
||||
const stateData = handleFormError(ex, formData);
|
||||
setFormData({ ...stateData });
|
||||
}
|
||||
});
|
||||
callback?.(res.info);
|
||||
});
|
||||
};
|
||||
|
||||
const clickBtn = () => {
|
||||
|
|
|
@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import Pattern from '@/common/pattern';
|
||||
import { Pagination } from '@/components';
|
||||
import { loggedUserInfoStore, toastStore } from '@/stores';
|
||||
import { scrollTop } from '@/utils';
|
||||
import { scrollToElementTop } from '@/utils';
|
||||
import { usePageTags, usePageUsers } from '@/hooks';
|
||||
import type {
|
||||
ListResult,
|
||||
|
@ -80,7 +80,7 @@ const Index = () => {
|
|||
if (page > 0 || order) {
|
||||
// scroll into view;
|
||||
const element = document.getElementById('answerHeader');
|
||||
scrollTop(element);
|
||||
scrollToElementTop(element);
|
||||
}
|
||||
|
||||
res.list.forEach((item) => {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import React, { useState, useRef } from 'react';
|
||||
import { Container, Row, Col, Form, Button, Card } from 'react-bootstrap';
|
||||
import { useParams, useNavigate } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next';
|
|||
import dayjs from 'dayjs';
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { handleFormError } from '@/utils';
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Editor, EditorRef, Icon } from '@/components';
|
||||
|
@ -19,11 +20,11 @@ import {
|
|||
import './index.scss';
|
||||
|
||||
interface FormDataItem {
|
||||
answer: Type.FormValue<string>;
|
||||
content: Type.FormValue<string>;
|
||||
description: Type.FormValue<string>;
|
||||
}
|
||||
const initFormData = {
|
||||
answer: {
|
||||
content: {
|
||||
value: '',
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
|
@ -35,7 +36,6 @@ const initFormData = {
|
|||
},
|
||||
};
|
||||
const Index = () => {
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
const { aid = '', qid = '' } = useParams();
|
||||
const [focusType, setForceType] = useState('');
|
||||
|
||||
|
@ -43,6 +43,10 @@ const Index = () => {
|
|||
const navigate = useNavigate();
|
||||
|
||||
const { data } = useQueryAnswerInfo(aid);
|
||||
const [formData, setFormData] = useState<FormDataItem>(initFormData);
|
||||
|
||||
initFormData.content.value = data?.info.content || '';
|
||||
|
||||
const { data: revisions = [] } = useQueryRevisions(aid);
|
||||
|
||||
const editorRef = useRef<EditorRef>({
|
||||
|
@ -51,18 +55,10 @@ const Index = () => {
|
|||
|
||||
const questionContentRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
formData.answer.value = data.info.content;
|
||||
setFormData({ ...formData });
|
||||
}, [data]);
|
||||
|
||||
const handleAnswerChange = (value: string) =>
|
||||
setFormData({
|
||||
...formData,
|
||||
answer: { ...formData.answer, value },
|
||||
content: { ...formData.content, value },
|
||||
});
|
||||
const handleSummaryChange = (evt) => {
|
||||
const v = evt.currentTarget.value;
|
||||
|
@ -74,18 +70,18 @@ const Index = () => {
|
|||
|
||||
const checkValidated = (): boolean => {
|
||||
let bol = true;
|
||||
const { answer } = formData;
|
||||
const { content } = formData;
|
||||
|
||||
if (!answer.value) {
|
||||
if (!content.value || Array.from(content.value.trim()).length < 6) {
|
||||
bol = false;
|
||||
formData.answer = {
|
||||
value: '',
|
||||
formData.content = {
|
||||
value: content.value,
|
||||
isInvalid: true,
|
||||
errorMsg: '标题不能为空',
|
||||
errorMsg: t('form.fields.answer.feedback.characters'),
|
||||
};
|
||||
} else {
|
||||
formData.answer = {
|
||||
value: answer.value,
|
||||
formData.content = {
|
||||
value: content.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
};
|
||||
|
@ -105,29 +101,36 @@ const Index = () => {
|
|||
}
|
||||
|
||||
const params: Type.AnswerParams = {
|
||||
content: formData.answer.value,
|
||||
content: formData.content.value,
|
||||
html: editorRef.current.getHtml(),
|
||||
question_id: qid,
|
||||
id: aid,
|
||||
edit_summary: formData.description.value,
|
||||
};
|
||||
modifyAnswer(params).then((res) => {
|
||||
navigate(
|
||||
pathFactory.answerLanding({
|
||||
questionId: qid,
|
||||
slugTitle: data?.question?.url_title,
|
||||
answerId: aid,
|
||||
}),
|
||||
{
|
||||
state: { isReview: res?.wait_for_review },
|
||||
},
|
||||
);
|
||||
});
|
||||
modifyAnswer(params)
|
||||
.then((res) => {
|
||||
navigate(
|
||||
pathFactory.answerLanding({
|
||||
questionId: qid,
|
||||
slugTitle: data?.question?.url_title,
|
||||
answerId: aid,
|
||||
}),
|
||||
{
|
||||
state: { isReview: res?.wait_for_review },
|
||||
},
|
||||
);
|
||||
})
|
||||
.catch((ex) => {
|
||||
if (ex.isError) {
|
||||
const stateData = handleFormError(ex, formData);
|
||||
setFormData({ ...stateData });
|
||||
}
|
||||
});
|
||||
};
|
||||
const handleSelectedRevision = (e) => {
|
||||
const index = e.target.value;
|
||||
const revision = revisions[index];
|
||||
formData.answer.value = revision.content.content;
|
||||
formData.content.value = revision.content.content;
|
||||
setFormData({ ...formData });
|
||||
};
|
||||
|
||||
|
@ -190,7 +193,7 @@ const Index = () => {
|
|||
<Form.Group controlId="answer" className="mt-3">
|
||||
<Form.Label>{t('form.fields.answer.label')}</Form.Label>
|
||||
<Editor
|
||||
value={formData.answer.value}
|
||||
value={formData.content.value}
|
||||
onChange={handleAnswerChange}
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
|
@ -205,14 +208,14 @@ const Index = () => {
|
|||
ref={editorRef}
|
||||
/>
|
||||
<Form.Control
|
||||
value={formData.answer.value}
|
||||
value={formData.content.value}
|
||||
type="text"
|
||||
isInvalid={formData.answer.isInvalid}
|
||||
isInvalid={formData.content.isInvalid}
|
||||
readOnly
|
||||
hidden
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.answer.errorMsg}
|
||||
{formData.content.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="edit_summary" className="my-3">
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ListGroupItem } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
import { Icon, Tag, FormatTime, BaseUserCard } from '@/components';
|
||||
import { Tag, FormatTime, BaseUserCard, Counts } from '@/components';
|
||||
import type { SearchResItem } from '@/common/interface';
|
||||
import { escapeRemove } from '@/utils';
|
||||
|
||||
|
@ -51,23 +51,17 @@ const Index: FC<Props> = ({ data }) => {
|
|||
className="me-3"
|
||||
preFix={data.object_type === 'question' ? 'asked' : 'answered'}
|
||||
/>
|
||||
<div className="d-flex align-items-center my-2 my-sm-0">
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="hand-thumbs-up-fill me-1" />
|
||||
<span> {data.object?.vote_count}</span>
|
||||
</div>
|
||||
<div
|
||||
className={`d-flex align-items-center ${
|
||||
data.object?.accepted ? 'text-success' : ''
|
||||
}`}>
|
||||
{data.object?.accepted ? (
|
||||
<Icon name="check-circle-fill me-1" />
|
||||
) : (
|
||||
<Icon name="chat-square-text-fill me-1" />
|
||||
)}
|
||||
<span>{data.object?.answer_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Counts
|
||||
className="my-2 my-sm-0"
|
||||
showViews={false}
|
||||
isAccepted={data.object?.accepted}
|
||||
data={{
|
||||
votes: data.object?.vote_count,
|
||||
answers: data.object?.answer_count,
|
||||
views: 0,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{data.object?.excerpt && (
|
||||
|
|
|
@ -3,10 +3,12 @@ import { Container, Row, Col } from 'react-bootstrap';
|
|||
import { Link, useLocation } from 'react-router-dom';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'account_result' });
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
const location = useLocation();
|
||||
usePageTags({
|
||||
title: t('account_activation', { keyPrefix: 'page_title' }),
|
||||
|
@ -15,7 +17,9 @@ const Index: FC = () => {
|
|||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col lg={6}>
|
||||
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
|
||||
<h3 className="text-center mt-3 mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
{location.pathname?.includes('success') && (
|
||||
<>
|
||||
<p className="text-center">{t('success')}</p>
|
||||
|
|
|
@ -2,18 +2,22 @@ import { FC, memo } from 'react';
|
|||
import { Container, Col } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { siteInfoStore } from '@/stores';
|
||||
import { usePageTags } from '@/hooks';
|
||||
|
||||
import SendEmail from './components/sendEmail';
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'change_email' });
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
usePageTags({
|
||||
title: t('change_email', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '6rem' }}>
|
||||
<h3 className="text-center mb-5">{t('page_title')}</h3>
|
||||
<h3 className="text-center mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
<Col className="mx-auto" md={3}>
|
||||
<SendEmail />
|
||||
</Col>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { Link, useSearchParams } from 'react-router-dom';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { loggedUserInfoStore } from '@/stores';
|
||||
import { loggedUserInfoStore, siteInfoStore } from '@/stores';
|
||||
import { changeEmailVerify, getLoggedUserInfo } from '@/services';
|
||||
|
||||
const Index: FC = () => {
|
||||
|
@ -13,6 +13,7 @@ const Index: FC = () => {
|
|||
const [step, setStep] = useState('loading');
|
||||
|
||||
const updateUser = loggedUserInfoStore((state) => state.update);
|
||||
const siteName = siteInfoStore((state) => state.siteInfo.name);
|
||||
|
||||
useEffect(() => {
|
||||
const code = searchParams.get('code');
|
||||
|
@ -38,7 +39,9 @@ const Index: FC = () => {
|
|||
<Container className="pt-4 mt-2 mb-5">
|
||||
<Row className="justify-content-center">
|
||||
<Col lg={6}>
|
||||
<h3 className="text-center mt-3 mb-5">{t('page_title')}</h3>
|
||||
<h3 className="text-center mt-3 mb-5">
|
||||
{t('page_title', { site_name: siteName })}
|
||||
</h3>
|
||||
{step === 'success' && (
|
||||
<>
|
||||
<p className="text-center">{t('confirm_new_email')}</p>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
|||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon, FormatTime, Tag } from '@/components';
|
||||
import { FormatTime, Tag, Counts } from '@/components';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface Props {
|
||||
|
@ -35,21 +35,16 @@ const Index: FC<Props> = ({ visible, data }) => {
|
|||
<div className="d-flex align-items-center fs-14 text-secondary mb-2">
|
||||
<FormatTime
|
||||
time={item.create_time}
|
||||
className="me-4"
|
||||
className="me-3"
|
||||
preFix={t('answered')}
|
||||
/>
|
||||
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="hand-thumbs-up-fill me-1" />
|
||||
<span>{item?.vote_count}</span>
|
||||
</div>
|
||||
|
||||
{item.accepted === 2 && (
|
||||
<div className="d-flex align-items-center me-3 text-success">
|
||||
<Icon name="check-circle-fill me-1" />
|
||||
<span>{t('accepted')}</span>
|
||||
</div>
|
||||
)}
|
||||
<Counts
|
||||
data={{ votes: item?.vote_count, views: 0, answers: 0 }}
|
||||
showAnswers={false}
|
||||
showViews={false}
|
||||
showAccepted={item.accepted === 2}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{item.question_info?.tags?.map((tag) => {
|
||||
|
|
|
@ -2,7 +2,7 @@ import { FC, memo } from 'react';
|
|||
import { ListGroup, ListGroupItem } from 'react-bootstrap';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Icon, FormatTime, Tag, BaseUserCard } from '@/components';
|
||||
import { FormatTime, Tag, BaseUserCard, Counts } from '@/components';
|
||||
import { pathFactory } from '@/router/pathFactory';
|
||||
|
||||
interface Props {
|
||||
|
@ -44,35 +44,23 @@ const Index: FC<Props> = ({ visible, tabName, data }) => {
|
|||
<span className="split-dot" />
|
||||
</>
|
||||
)}
|
||||
|
||||
<FormatTime
|
||||
time={item.create_time}
|
||||
time={
|
||||
tabName === 'bookmarks' ? item.create_time : item.created_at
|
||||
}
|
||||
className="me-3"
|
||||
preFix={t('asked')}
|
||||
/>
|
||||
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="hand-thumbs-up-fill me-1" />
|
||||
<span>{item.vote_count}</span>
|
||||
</div>
|
||||
|
||||
{tabName !== 'answers' && (
|
||||
<div
|
||||
className={`d-flex align-items-center me-3 ${
|
||||
Number(item.accepted_answer_id) > 0 ? 'text-success' : ''
|
||||
}`}>
|
||||
{Number(item.accepted_answer_id) > 0 ? (
|
||||
<Icon name="check-circle-fill me-1" />
|
||||
) : (
|
||||
<Icon name="chat-square-text-fill me-1" />
|
||||
)}
|
||||
<span>{item.answer_count}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="d-flex align-items-center me-3">
|
||||
<Icon name="eye-fill me-1" />
|
||||
<span>{item.view_count}</span>
|
||||
</div>
|
||||
<Counts
|
||||
isAccepted={Number(item.accepted_answer_id) > 0}
|
||||
data={{
|
||||
votes: item.vote_count,
|
||||
answers: item.answer_count,
|
||||
views: item.view_count,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
{item.tags?.map((tag) => {
|
||||
|
|
|
@ -32,9 +32,12 @@ const Index: FC<Props> = ({ data, type }) => {
|
|||
}>
|
||||
{type === 'answer' ? item.question_info.title : item.title}
|
||||
</a>
|
||||
|
||||
<div className="d-inline-block text-secondary ms-3 fs-14">
|
||||
<Icon name="hand-thumbs-up-fill" />
|
||||
<span> {item.vote_count}</span>
|
||||
<Icon name="hand-thumbs-up-fill me-1" />
|
||||
<span>
|
||||
{item.vote_count} {t('votes', { keyPrefix: 'counts' })}
|
||||
</span>
|
||||
</div>
|
||||
{type === 'question' && (
|
||||
<div
|
||||
|
@ -47,7 +50,10 @@ const Index: FC<Props> = ({ data, type }) => {
|
|||
<Icon name="chat-square-text-fill" />
|
||||
)}
|
||||
|
||||
<span> {item.answer_count}</span>
|
||||
<span>
|
||||
{' '}
|
||||
{item.answer_count} {t('answers', { keyPrefix: 'counts' })}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import React, { FormEvent, useState, useEffect } from 'react';
|
|||
import { Form, Button, Stack, ButtonGroup } from 'react-bootstrap';
|
||||
import { Trans, useTranslation } from 'react-i18next';
|
||||
|
||||
import { marked } from 'marked';
|
||||
import MD5 from 'md5';
|
||||
|
||||
import type { FormDataType } from '@/common/interface';
|
||||
|
@ -205,7 +204,6 @@ const Index: React.FC = () => {
|
|||
bio: formData.bio.value,
|
||||
website: formData.website.value,
|
||||
location: formData.location.value,
|
||||
bio_html: marked.parse(formData.bio.value),
|
||||
};
|
||||
|
||||
modifyUserInfo(params)
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
import i18next from 'i18next';
|
||||
import parse from 'html-react-parser';
|
||||
import * as DOMPurify from 'dompurify';
|
||||
|
||||
const Diff = require('diff');
|
||||
|
||||
|
@ -21,7 +23,7 @@ function formatCount($num: number): string {
|
|||
return res;
|
||||
}
|
||||
|
||||
function scrollTop(element) {
|
||||
function scrollToElementTop(element) {
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
@ -36,6 +38,15 @@ function scrollTop(element) {
|
|||
});
|
||||
}
|
||||
|
||||
const scrollToDocTop = () => {
|
||||
setTimeout(() => {
|
||||
window.scrollTo({
|
||||
top: 0,
|
||||
behavior: 'smooth',
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const bgFadeOut = (el) => {
|
||||
if (el && !el.classList.contains('bg-fade-out')) {
|
||||
el.classList.add('bg-fade-out');
|
||||
|
@ -160,14 +171,17 @@ function handleFormError(
|
|||
) {
|
||||
if (error.list?.length > 0) {
|
||||
error.list.forEach((item) => {
|
||||
data[item.error_field].isInvalid = true;
|
||||
data[item.error_field].errorMsg = item.error_msg;
|
||||
const errorFieldObject = data[item.error_field];
|
||||
if (errorFieldObject) {
|
||||
errorFieldObject.isInvalid = true;
|
||||
errorFieldObject.errorMsg = item.error_msg;
|
||||
}
|
||||
});
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
function diffText(newText: string, oldText: string): string {
|
||||
function diffText(newText: string, oldText?: string): string {
|
||||
if (!newText) {
|
||||
return '';
|
||||
}
|
||||
|
@ -181,8 +195,6 @@ function diffText(newText: string, oldText: string): string {
|
|||
?.replace(/<input/gi, '<input');
|
||||
}
|
||||
const diff = Diff.diffChars(oldText, newText);
|
||||
console.log(diff);
|
||||
|
||||
const result = diff.map((part) => {
|
||||
if (part.added) {
|
||||
if (part.value.replace(/\n/g, '').length <= 0) {
|
||||
|
@ -214,10 +226,18 @@ function diffText(newText: string, oldText: string): string {
|
|||
?.replace(/<input/gi, '<input');
|
||||
}
|
||||
|
||||
function htmlToReact(html: string) {
|
||||
const cleanedHtml = DOMPurify.sanitize(html, {
|
||||
USE_PROFILES: { html: true },
|
||||
});
|
||||
return parse(cleanedHtml);
|
||||
}
|
||||
|
||||
export {
|
||||
thousandthDivision,
|
||||
formatCount,
|
||||
scrollTop,
|
||||
scrollToElementTop,
|
||||
scrollToDocTop,
|
||||
bgFadeOut,
|
||||
matchedUsers,
|
||||
parseUserInfo,
|
||||
|
@ -228,4 +248,5 @@ export {
|
|||
labelStyle,
|
||||
handleFormError,
|
||||
diffText,
|
||||
htmlToReact,
|
||||
};
|
||||
|
|
|
@ -267,7 +267,7 @@ export const initAppSettingsStore = async () => {
|
|||
};
|
||||
|
||||
export const shouldInitAppFetchData = () => {
|
||||
if (isIgnoredPath('/install')) {
|
||||
if (isIgnoredPath('/install') && window.location.pathname === '/install') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -48,7 +48,7 @@ class Request {
|
|||
},
|
||||
(error) => {
|
||||
const { status, data: respData } = error.response || {};
|
||||
const { data = {}, msg = '' } = respData || {};
|
||||
const { data = {}, msg = '', reason = '' } = respData || {};
|
||||
if (status === 400) {
|
||||
// show error message
|
||||
if (data instanceof Object && data.err_type) {
|
||||
|
@ -79,7 +79,13 @@ class Request {
|
|||
|
||||
if (data instanceof Array && data.length > 0) {
|
||||
// handle form error
|
||||
return Promise.reject({ isError: true, list: data });
|
||||
return Promise.reject({
|
||||
code: status,
|
||||
msg,
|
||||
reason,
|
||||
isError: true,
|
||||
list: data,
|
||||
});
|
||||
}
|
||||
|
||||
if (!data || Object.keys(data).length <= 0) {
|
||||
|
|
|
@ -6,9 +6,9 @@
|
|||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<title>{{.title}}</title>
|
||||
<meta name="description" content="{{.description}}" data-rh="true" />
|
||||
{{if .keywords }}
|
||||
<meta name="keywords" content="{{.keywords}}" data-rh="true" />
|
||||
{{end}}
|
||||
<meta name="generator" content="Answer {{.Version}} - https://github.com/answerdev/answer">
|
||||
{{if .keywords }}<meta name="keywords" content="{{.keywords}}" data-rh="true" />{{end}}
|
||||
|
||||
<link rel="canonical" href="{{.siteinfo.Canonical}}" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<link href="{{.cssPath}}" rel="stylesheet" />
|
||||
|
|
Loading…
Reference in New Issue