mirror of https://gitee.com/answerdev/answer.git
Merge branch 'github-main' into feat/1.2.0/state
# Conflicts: # cmd/wire_gen.go # internal/service/question_common/question.go # internal/service/siteinfo/siteinfo_service.go
This commit is contained in:
commit
c49fa00570
|
@ -1,64 +0,0 @@
|
|||
FROM amd64/node:18 AS node-builder
|
||||
|
||||
LABEL maintainer="mingcheng<mc@sf.com>"
|
||||
|
||||
COPY . /answer
|
||||
WORKDIR /answer
|
||||
RUN node -v
|
||||
RUN make install-ui-packages ui && mv ui/build /tmp
|
||||
|
||||
# stage2 build the main binary within static resource
|
||||
FROM golang:1.19-alpine AS golang-builder
|
||||
LABEL maintainer="aichy@sf.com"
|
||||
|
||||
ARG GOPROXY
|
||||
# ENV GOPROXY ${GOPROXY:-direct}
|
||||
ENV GOPROXY=https://goproxy.io,direct
|
||||
|
||||
ENV GOPATH /go
|
||||
ENV GOROOT /usr/local/go
|
||||
ENV PACKAGE github.com/answerdev/answer
|
||||
ENV BUILD_DIR ${GOPATH}/src/${PACKAGE}
|
||||
ENV ANSWER_MODULE ${BUILD_DIR}
|
||||
|
||||
ARG TAGS="sqlite sqlite_unlock_notify"
|
||||
ENV TAGS "bindata timetzdata $TAGS"
|
||||
ARG CGO_EXTRA_CFLAGS
|
||||
|
||||
COPY . ${BUILD_DIR}
|
||||
WORKDIR ${BUILD_DIR}
|
||||
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
|
||||
RUN apk --no-cache add build-base git bash \
|
||||
&& make clean build
|
||||
RUN chmod 755 answer
|
||||
RUN ["/bin/bash","-c","script/build_plugin.sh"]
|
||||
RUN cp answer /usr/bin/answer
|
||||
|
||||
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
|
||||
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||
|
||||
# stage3 copy the binary and resource files into fresh container
|
||||
FROM alpine
|
||||
LABEL maintainer="maintainers@sf.com"
|
||||
|
||||
ENV TZ "Asia/Shanghai"
|
||||
RUN apk update \
|
||||
&& apk --no-cache add \
|
||||
bash \
|
||||
ca-certificates \
|
||||
curl \
|
||||
dumb-init \
|
||||
gettext \
|
||||
openssh \
|
||||
sqlite \
|
||||
gnupg \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone
|
||||
|
||||
COPY --from=golang-builder /usr/bin/answer /usr/bin/answer
|
||||
COPY --from=golang-builder /data /data
|
||||
COPY /script/entrypoint.sh /entrypoint.sh
|
||||
RUN chmod 755 /entrypoint.sh
|
||||
|
||||
VOLUME /data
|
||||
EXPOSE 80
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
|
@ -40,7 +40,7 @@ jobs:
|
|||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
file: ./.github/Dockerfile
|
||||
file: ./Dockerfile
|
||||
platforms: linux/amd64
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
|
|
|
@ -39,20 +39,13 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: ./.github/Dockerfile
|
||||
file: ./Dockerfile
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
|
|
@ -51,20 +51,13 @@ jobs:
|
|||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
# - name: Login to GitHub Container Registry
|
||||
# uses: docker/login-action@v2
|
||||
# with:
|
||||
# registry: ghcr.io
|
||||
# username: ${{ github.actor }}
|
||||
# password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
with:
|
||||
context: .
|
||||
platforms: linux/amd64,linux/arm64
|
||||
push: true
|
||||
file: ./.github/Dockerfile
|
||||
file: ./Dockerfile
|
||||
tags: answerdev/answer:${{ inputs.tag_name }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
|
||||
|
|
18
Dockerfile
18
Dockerfile
|
@ -1,14 +1,3 @@
|
|||
# FROM amd64/node AS node-builder
|
||||
FROM amd64/node:18 AS node-builder
|
||||
|
||||
LABEL maintainer="mingcheng<mc@sf.com>"
|
||||
|
||||
COPY . /answer
|
||||
WORKDIR /answer
|
||||
RUN node -v
|
||||
RUN make install-ui-packages ui && mv ui/build /tmp
|
||||
|
||||
# stage2 build the main binary within static resource
|
||||
FROM golang:1.19-alpine AS golang-builder
|
||||
LABEL maintainer="aichy@sf.com"
|
||||
|
||||
|
@ -28,9 +17,9 @@ ARG CGO_EXTRA_CFLAGS
|
|||
|
||||
COPY . ${BUILD_DIR}
|
||||
WORKDIR ${BUILD_DIR}
|
||||
COPY --from=node-builder /tmp/build ${BUILD_DIR}/ui/build
|
||||
RUN apk --no-cache add build-base git bash \
|
||||
&& make clean build
|
||||
RUN apk --no-cache add build-base git bash nodejs npm && npm install -g pnpm corepack \
|
||||
&& make install-ui-packages clean build
|
||||
|
||||
RUN chmod 755 answer
|
||||
RUN ["/bin/bash","-c","script/build_plugin.sh"]
|
||||
RUN cp answer /usr/bin/answer
|
||||
|
@ -38,7 +27,6 @@ RUN cp answer /usr/bin/answer
|
|||
RUN mkdir -p /data/uploads && chmod 777 /data/uploads \
|
||||
&& mkdir -p /data/i18n && cp -r i18n/*.yaml /data/i18n
|
||||
|
||||
# stage3 copy the binary and resource files into fresh container
|
||||
FROM alpine
|
||||
LABEL maintainer="maintainers@sf.com"
|
||||
|
||||
|
|
|
@ -169,7 +169,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
answerCommon := answercommon.NewAnswerCommon(answerRepo)
|
||||
metaRepo := meta.NewMetaRepo(dataData)
|
||||
metaService := meta2.NewMetaService(metaRepo)
|
||||
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService)
|
||||
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService, dataData)
|
||||
collectionService := service.NewCollectionService(collectionRepo, collectionGroupRepo, questionCommon)
|
||||
collectionController := controller.NewCollectionController(collectionService)
|
||||
answerActivityRepo := activity.NewAnswerActivityRepo(dataData, activityRepo, userRankRepo, notificationQueueService)
|
||||
|
@ -196,7 +196,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
|
|||
reasonService := reason2.NewReasonService(reasonRepo)
|
||||
reasonController := controller.NewReasonController(reasonService)
|
||||
themeController := controller_admin.NewThemeController()
|
||||
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService)
|
||||
siteInfoService := siteinfo.NewSiteInfoService(siteInfoRepo, siteInfoCommonService, emailService, tagCommonService, configService, questionCommon)
|
||||
siteInfoController := controller_admin.NewSiteInfoController(siteInfoService)
|
||||
controllerSiteInfoController := controller.NewSiteInfoController(siteInfoCommonService)
|
||||
notificationRepo := notification.NewNotificationRepo(dataData)
|
||||
|
|
455
i18n/cs_CZ.yaml
455
i18n/cs_CZ.yaml
|
@ -11,6 +11,31 @@ backend:
|
|||
other: Unauthorized.
|
||||
database_error:
|
||||
other: Data server error.
|
||||
forbidden_error:
|
||||
other: Forbidden.
|
||||
action:
|
||||
report:
|
||||
other: Flag
|
||||
edit:
|
||||
other: Edit
|
||||
delete:
|
||||
other: Delete
|
||||
close:
|
||||
other: Close
|
||||
reopen:
|
||||
other: Reopen
|
||||
forbidden_error:
|
||||
other: Forbidden.
|
||||
pin:
|
||||
other: Pin
|
||||
hide:
|
||||
other: Unlist
|
||||
unpin:
|
||||
other: Unpin
|
||||
show:
|
||||
other: List
|
||||
invite_someone_to_answer:
|
||||
other: Edit
|
||||
role:
|
||||
name:
|
||||
user:
|
||||
|
@ -26,6 +51,60 @@ backend:
|
|||
other: Have the full power to access the site.
|
||||
moderator:
|
||||
other: Has access to all posts except admin settings.
|
||||
privilege:
|
||||
level_1:
|
||||
description:
|
||||
other: Level 1 (less reputation required for private team, group)
|
||||
level_2:
|
||||
description:
|
||||
other: Level 2 (low reputation required for startup community)
|
||||
level_3:
|
||||
description:
|
||||
other: Level 3 (high reputation required for mature community)
|
||||
rank_question_add_label:
|
||||
other: Ask question
|
||||
rank_answer_add_label:
|
||||
other: Write answer
|
||||
rank_comment_add_label:
|
||||
other: Write comment
|
||||
rank_report_add_label:
|
||||
other: Flag
|
||||
rank_comment_vote_up_label:
|
||||
other: Upvote comment
|
||||
rank_link_url_limit_label:
|
||||
other: Post more than 2 links at a time
|
||||
rank_question_vote_up_label:
|
||||
other: Upvote question
|
||||
rank_answer_vote_up_label:
|
||||
other: Upvote answer
|
||||
rank_question_vote_down_label:
|
||||
other: Downvote question
|
||||
rank_answer_vote_down_label:
|
||||
other: Downvote answer
|
||||
rank_invite_someone_to_answer_label:
|
||||
other: Invite someone to answer
|
||||
rank_tag_add_label:
|
||||
other: Create new tag
|
||||
rank_tag_edit_label:
|
||||
other: Edit tag description (need to review)
|
||||
rank_question_edit_label:
|
||||
other: Edit other's question (need to review)
|
||||
rank_answer_edit_label:
|
||||
other: Edit other's answer (need to review)
|
||||
rank_question_edit_without_review_label:
|
||||
other: Edit other's question without review
|
||||
rank_answer_edit_without_review_label:
|
||||
other: Edit other's answer without review
|
||||
rank_question_audit_label:
|
||||
other: Review question edits
|
||||
rank_answer_audit_label:
|
||||
other: Review answer edits
|
||||
rank_tag_audit_label:
|
||||
other: Review tag edits
|
||||
rank_tag_edit_without_review_label:
|
||||
other: Edit tag description without review
|
||||
rank_tag_synonym_label:
|
||||
other: Manage tag synonyms
|
||||
email:
|
||||
other: Email
|
||||
password:
|
||||
|
@ -33,7 +112,14 @@ backend:
|
|||
email_or_password_wrong_error:
|
||||
other: Email and password do not match.
|
||||
error:
|
||||
password:
|
||||
space_invalid:
|
||||
other: Password cannot contain spaces.
|
||||
admin:
|
||||
cannot_update_their_password:
|
||||
other: You cannot modify your password.
|
||||
cannot_modify_self_status:
|
||||
other: You cannot modify your status.
|
||||
email_or_password_wrong:
|
||||
other: Email and password do not match.
|
||||
answer:
|
||||
|
@ -43,6 +129,8 @@ backend:
|
|||
other: No permission to delete.
|
||||
cannot_update:
|
||||
other: No permission to update.
|
||||
question_closed_cannot_add:
|
||||
other: Questions are closed and cannot be added.
|
||||
comment:
|
||||
edit_without_permission:
|
||||
other: Comment are not allowed to edit.
|
||||
|
@ -57,6 +145,8 @@ backend:
|
|||
other: Email should be verified.
|
||||
verify_url_expired:
|
||||
other: Email verified URL has expired, please resend the email.
|
||||
illegal_email_domain_error:
|
||||
other: Email is not allowed from that email domain. Please use another one.
|
||||
lang:
|
||||
not_found:
|
||||
other: Language file not found.
|
||||
|
@ -80,6 +170,8 @@ backend:
|
|||
new_password_same_as_previous_setting:
|
||||
other: The new password is the same as the previous one.
|
||||
question:
|
||||
already_deleted:
|
||||
other: This post has been deleted.
|
||||
not_found:
|
||||
other: Question not found.
|
||||
cannot_deleted:
|
||||
|
@ -91,12 +183,18 @@ backend:
|
|||
rank:
|
||||
fail_to_meet_the_condition:
|
||||
other: Rank fail to meet the condition.
|
||||
vote_fail_to_meet_the_condition:
|
||||
other: Thanks for the feedback. You need at least {{.Rank}} reputation to cast a vote.
|
||||
no_enough_rank_to_operate:
|
||||
other: You need at least {{.Rank}} reputation to do this.
|
||||
report:
|
||||
handle_failed:
|
||||
other: Report handle failed.
|
||||
not_found:
|
||||
other: Report not found.
|
||||
tag:
|
||||
already_exist:
|
||||
other: Tag already exists.
|
||||
not_found:
|
||||
other: Tag not found.
|
||||
recommend_tag_not_found:
|
||||
|
@ -107,6 +205,8 @@ backend:
|
|||
other: Should not contain synonym tags.
|
||||
cannot_update:
|
||||
other: No permission to update.
|
||||
is_used_cannot_delete:
|
||||
other: You cannot delete a tag that is in use
|
||||
cannot_set_synonym_as_itself:
|
||||
other: You cannot set the synonym of the current tag as itself.
|
||||
smtp:
|
||||
|
@ -121,6 +221,10 @@ backend:
|
|||
no_permission:
|
||||
other: No permission to Revision.
|
||||
user:
|
||||
external_login_missing_user_id:
|
||||
other: The third-party platform does not provide a unique UserID, so you cannot login, please contact the website administrator.
|
||||
external_login_unbinding_forbidden:
|
||||
other: Please set a login password for your account before you remove this login.
|
||||
email_or_password_wrong:
|
||||
other:
|
||||
other: Email and password do not match.
|
||||
|
@ -138,6 +242,10 @@ backend:
|
|||
other: You cannot modify your role.
|
||||
not_allowed_registration:
|
||||
other: Currently the site is not open for registration
|
||||
access_denied:
|
||||
other: Access denied
|
||||
page_access_denied:
|
||||
other: You do not have access to this page.
|
||||
config:
|
||||
read_config_failed:
|
||||
other: Read config failed
|
||||
|
@ -152,37 +260,74 @@ backend:
|
|||
upload:
|
||||
unsupported_file_format:
|
||||
other: Unsupported file format.
|
||||
report:
|
||||
site_info:
|
||||
config_not_found:
|
||||
other: Site config not found.
|
||||
reason:
|
||||
spam:
|
||||
name:
|
||||
other: spam
|
||||
desc:
|
||||
other: This post is an advertisement, or vandalism. It is not useful or relevant to the current topic.
|
||||
rude:
|
||||
rude_or_abusive:
|
||||
name:
|
||||
other: rude or abusive
|
||||
desc:
|
||||
other: A reasonable person would find this content inappropriate for respectful discourse.
|
||||
duplicate:
|
||||
a_duplicate:
|
||||
name:
|
||||
other: a duplicate
|
||||
desc:
|
||||
other: This question has been asked before and already has an answer.
|
||||
not_answer:
|
||||
placeholder:
|
||||
other: Enter the existing question link
|
||||
not_a_answer:
|
||||
name:
|
||||
other: not an answer
|
||||
desc:
|
||||
other: This was posted as an answer, but it does not attempt to answer the question. It should possibly be an edit, a comment, another question, or deleted altogether.
|
||||
not_need:
|
||||
no_longer_needed:
|
||||
name:
|
||||
other: no longer needed
|
||||
desc:
|
||||
other: This comment is outdated, conversational or not relevant to this post.
|
||||
other:
|
||||
something:
|
||||
name:
|
||||
other: something else
|
||||
desc:
|
||||
other: This post requires staff attention for another reason not listed above.
|
||||
placeholder:
|
||||
other: Let us know specifically what you are concerned about
|
||||
community_specific:
|
||||
name:
|
||||
other: a community-specific reason
|
||||
desc:
|
||||
other: This question doesn’t meet a community guideline.
|
||||
not_clarity:
|
||||
name:
|
||||
other: needs details or clarity
|
||||
desc:
|
||||
other: This question currently includes multiple questions in one. It should focus on one problem only.
|
||||
looks_ok:
|
||||
name:
|
||||
other: looks ok
|
||||
desc:
|
||||
other: This post is good as-is and not low quality.
|
||||
needs_edit:
|
||||
name:
|
||||
other: needs edit, and I did it
|
||||
desc:
|
||||
other: Improve and correct problems with this post yourself.
|
||||
needs_close:
|
||||
name:
|
||||
other: needs close
|
||||
desc:
|
||||
other: A closed question can’t answer, but still can edit, vote and comment.
|
||||
needs_delete:
|
||||
name:
|
||||
other: needs delete
|
||||
desc:
|
||||
other: This post will be deleted.
|
||||
question:
|
||||
close:
|
||||
duplicate:
|
||||
|
@ -212,6 +357,8 @@ backend:
|
|||
other: answered
|
||||
modified:
|
||||
other: modified
|
||||
deleted_title:
|
||||
other: Deleted question
|
||||
notification:
|
||||
action:
|
||||
update_question:
|
||||
|
@ -238,6 +385,67 @@ backend:
|
|||
other: Your answer has been deleted
|
||||
your_comment_was_deleted:
|
||||
other: Your comment has been deleted
|
||||
up_voted_question:
|
||||
other: upvoted question
|
||||
down_voted_question:
|
||||
other: downvoted question
|
||||
up_voted_answer:
|
||||
other: upvoted answer
|
||||
down_voted_answer:
|
||||
other: downvoted answer
|
||||
up_voted_comment:
|
||||
other: upvoted comment
|
||||
invited_you_to_answer:
|
||||
other: invited you to answer
|
||||
email_tpl:
|
||||
change_email:
|
||||
title:
|
||||
other: "[{{.SiteName}}] Confirm your new email address"
|
||||
body:
|
||||
other: "Confirm your new email address for {{.SiteName}} by clicking on the following link:<br><br>\n\n<a href='{{.ChangeEmailUrl}}' target='_blank'>{{.ChangeEmailUrl}}</a><br><br>\n\nIf you did not request this change, please ignore this email.\n"
|
||||
new_answer:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} answered your question"
|
||||
body:
|
||||
other: "<strong><a href='{{.AnswerUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.AnswerSummary}}</blockquote><br>\n<a href='{{.AnswerUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
|
||||
invited_you_to_answer:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} invited you to answer"
|
||||
body:
|
||||
other: "<strong><a href='{{.InviteUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>I think you may know the answer.</blockquote><br>\n<a href='{{.InviteUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
|
||||
new_comment:
|
||||
title:
|
||||
other: "[{{.SiteName}}] {{.DisplayName}} commented on your post"
|
||||
body:
|
||||
other: "<strong><a href='{{.CommentUrl}}'>{{.QuestionTitle}}</a></strong><br><br>\n\n<small>{{.DisplayName}}:</small><br>\n<blockquote>{{.CommentSummary}}</blockquote><br>\n<a href='{{.CommentUrl}}'>View it on {{.SiteName}}</a><br><br>\n\n<small>You are receiving this because you authored the thread. <a href='{{.UnsubscribeUrl}}'>Unsubscribe</a></small>"
|
||||
pass_reset:
|
||||
title:
|
||||
other: "[{{.SiteName }}] Password reset"
|
||||
body:
|
||||
other: "Somebody asked to reset your password on [{{.SiteName}}].<br><br>\n\nIf it was not you, you can safely ignore this email.<br><br>\n\nClick the following link to choose a new password:<br>\n<a href='{{.PassResetUrl}}' target='_blank'>{{.PassResetUrl}}</a>\n"
|
||||
register:
|
||||
title:
|
||||
other: "[{{.SiteName}}] Confirm your new account"
|
||||
body:
|
||||
other: "Welcome to {{.SiteName}}<br><br>\n\nClick the following link to confirm and activate your new account:<br>\n<a href='{{.RegisterUrl}}' target='_blank'>{{.RegisterUrl}}</a><br><br>\n\nIf the above link is not clickable, try copying and pasting it into the address bar of your web browser.\n"
|
||||
test:
|
||||
title:
|
||||
other: "[{{.SiteName}}] Test Email"
|
||||
body:
|
||||
other: "This is a test email."
|
||||
action_activity_type:
|
||||
upvote:
|
||||
other: upvote
|
||||
upvoted:
|
||||
other: upvoted
|
||||
downvote:
|
||||
other: downvote
|
||||
downvoted:
|
||||
other: downvoted
|
||||
accept:
|
||||
other: accept
|
||||
accepted:
|
||||
other: accepted
|
||||
#The following fields are used for interface presentation(Front-end)
|
||||
ui:
|
||||
how_to_format:
|
||||
|
@ -253,6 +461,7 @@ ui:
|
|||
tag: Tag
|
||||
tags: Tags
|
||||
tag_wiki: tag wiki
|
||||
create_tag: Create Tag
|
||||
edit_tag: Edit Tag
|
||||
ask_a_question: Add Question
|
||||
edit_question: Edit Question
|
||||
|
@ -273,17 +482,28 @@ ui:
|
|||
upgrade: Answer Upgrade
|
||||
maintenance: Website Maintenance
|
||||
users: Users
|
||||
oauth_callback: Processing
|
||||
http_404: HTTP Error 404
|
||||
http_50X: HTTP Error 500
|
||||
http_403: HTTP Error 403
|
||||
notifications:
|
||||
title: Notifications
|
||||
inbox: Inbox
|
||||
achievement: Achievements
|
||||
all_read: Mark all as read
|
||||
show_more: Show more
|
||||
someone: Someone
|
||||
inbox_type:
|
||||
all: All
|
||||
posts: Posts
|
||||
invites: Invites
|
||||
votes: Votes
|
||||
suspended:
|
||||
title: Your Account has been Suspended
|
||||
until_time: "Your account was suspended until {{ time }}."
|
||||
forever: This user was suspended forever.
|
||||
end: You don't meet a community guideline.
|
||||
contact_us: Contact us
|
||||
editor:
|
||||
blockquote:
|
||||
text: Blockquote
|
||||
|
@ -309,7 +529,7 @@ ui:
|
|||
msg:
|
||||
empty: Code cannot be empty.
|
||||
language:
|
||||
label: Language (optional)
|
||||
label: Language
|
||||
placeholder: Automatic detection
|
||||
btn_cancel: Cancel
|
||||
btn_confirm: Add
|
||||
|
@ -345,7 +565,7 @@ ui:
|
|||
only_image: Only image files are allowed.
|
||||
max_size: File size cannot exceed 4MB.
|
||||
desc:
|
||||
label: Description (optional)
|
||||
label: Description
|
||||
tab_url: Image URL
|
||||
form_url:
|
||||
fields:
|
||||
|
@ -354,7 +574,7 @@ ui:
|
|||
msg:
|
||||
empty: Image URL cannot be empty.
|
||||
name:
|
||||
label: Description (optional)
|
||||
label: Description
|
||||
btn_cancel: Cancel
|
||||
btn_confirm: Add
|
||||
uploading: Uploading
|
||||
|
@ -374,7 +594,7 @@ ui:
|
|||
msg:
|
||||
empty: URL cannot be empty.
|
||||
name:
|
||||
label: Description (optional)
|
||||
label: Description
|
||||
btn_cancel: Cancel
|
||||
btn_confirm: Add
|
||||
ordered_list:
|
||||
|
@ -416,15 +636,16 @@ ui:
|
|||
range: Display name up to 35 characters.
|
||||
slug_name:
|
||||
label: URL Slug
|
||||
desc: 'Must use the character set "a-z", "0-9", "+ # - ."'
|
||||
desc: URL slug up to 35 characters.
|
||||
msg:
|
||||
empty: URL slug cannot be empty.
|
||||
range: URL slug up to 35 characters.
|
||||
character: URL slug contains unallowed character set.
|
||||
desc:
|
||||
label: Description (optional)
|
||||
label: Description
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
btn_post: Post new tag
|
||||
tag_info:
|
||||
created_at: Created
|
||||
edited_at: Edited
|
||||
|
@ -439,9 +660,11 @@ ui:
|
|||
synonyms_text: The following tags will be remapped to
|
||||
delete:
|
||||
title: Delete this tag
|
||||
content: >-
|
||||
<p>We do not allow deleting tag with posts.</p><p>Please remove this tag from the posts first.</p>
|
||||
content2: Are you sure you wish to delete?
|
||||
tip_with_posts: >-
|
||||
<p>We do not allowed <strong>deleting tag with posts</strong>.</p> <p>Please remove this tag from the posts first.</p>
|
||||
tip_with_synonyms: >-
|
||||
<p>We do not allowed <strong>deleting tag with synonyms</strong>.</p> <p>Please remove the synonyms from this tag first.</p>
|
||||
tip: Are you sure you wish to delete?
|
||||
close: Close
|
||||
edit_tag:
|
||||
title: Edit Tag
|
||||
|
@ -487,6 +710,7 @@ ui:
|
|||
Use comments to ask for more information or suggest improvements. Avoid answering questions in comments.
|
||||
tip_answer: >-
|
||||
Use comments to reply to other users or notify them of changes. If you are adding new information, edit your post instead of commenting.
|
||||
tip_vote: It adds something useful to the post
|
||||
edit_answer:
|
||||
title: Edit Answer
|
||||
default_reason: Edit answer
|
||||
|
@ -568,6 +792,8 @@ ui:
|
|||
logout: Log out
|
||||
admin: Admin
|
||||
review: Review
|
||||
bookmark: Bookmarks
|
||||
moderation: Moderation
|
||||
search:
|
||||
placeholder: Search
|
||||
footer:
|
||||
|
@ -592,7 +818,6 @@ ui:
|
|||
msg:
|
||||
empty: Cannot be empty.
|
||||
login:
|
||||
page_title: Welcome to {{site_name}}
|
||||
login_to_continue: Log in to continue
|
||||
info_sign: Don't have an account? <1>Sign up</1>
|
||||
info_login: Already have an account? <1>Log in</1>
|
||||
|
@ -603,6 +828,7 @@ ui:
|
|||
msg:
|
||||
empty: Name cannot be empty.
|
||||
range: Name up to 30 characters.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
email:
|
||||
label: Email
|
||||
msg:
|
||||
|
@ -622,7 +848,6 @@ ui:
|
|||
msg:
|
||||
empty: Email cannot be empty.
|
||||
change_email:
|
||||
page_title: Welcome to {{site_name}}
|
||||
btn_cancel: Cancel
|
||||
btn_update: Update email address
|
||||
send_success: >-
|
||||
|
@ -631,6 +856,20 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
email:
|
||||
label: Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
modal_title: Email already existes.
|
||||
modal_content: This email address already registered. Are you sure you want to connect to the existing account?
|
||||
modal_cancel: Change email
|
||||
modal_confirm: Connect to the existing account
|
||||
password_reset:
|
||||
page_title: Password Reset
|
||||
btn_name: Reset my password
|
||||
|
@ -649,6 +888,7 @@ ui:
|
|||
label: Confirm New Password
|
||||
settings:
|
||||
page_title: Settings
|
||||
goto_modify: Go to Modify
|
||||
nav:
|
||||
profile: Profile
|
||||
notification: Notifications
|
||||
|
@ -670,20 +910,20 @@ ui:
|
|||
avatar:
|
||||
label: Profile Image
|
||||
gravatar: Gravatar
|
||||
gravatar_text: You can change image on <1>gravatar.com</1>
|
||||
gravatar_text: You can change image on
|
||||
custom: Custom
|
||||
btn_refresh: Refresh
|
||||
custom_text: You can upload your image.
|
||||
default: System
|
||||
msg: Please upload an avatar
|
||||
bio:
|
||||
label: About Me (optional)
|
||||
label: About Me
|
||||
website:
|
||||
label: Website (optional)
|
||||
label: Website
|
||||
placeholder: "https://example.com"
|
||||
msg: Website incorrect format
|
||||
location:
|
||||
label: Location (optional)
|
||||
label: Location
|
||||
placeholder: "City, Country"
|
||||
notification:
|
||||
heading: Notifications
|
||||
|
@ -697,8 +937,11 @@ ui:
|
|||
change_email_info: >-
|
||||
We've sent an email to that address. Please follow the confirmation instructions.
|
||||
email:
|
||||
label: Email
|
||||
msg: Email cannot be empty.
|
||||
label: New Email
|
||||
msg: New Email cannot be empty.
|
||||
pass:
|
||||
label: Current Password
|
||||
msg: Password cannot be empty.
|
||||
password_title: Password
|
||||
current_pass:
|
||||
label: Current Password
|
||||
|
@ -715,6 +958,13 @@ ui:
|
|||
lang:
|
||||
label: Interface Language
|
||||
text: User interface language. It will change when you refresh the page.
|
||||
my_logins:
|
||||
title: My Logins
|
||||
label: Log in or sign up on this site using these accounts.
|
||||
modal_title: Remove Login
|
||||
modal_content: Are you sure you want to remove this login from your account?
|
||||
modal_confirm_btn: Remove
|
||||
remove_success: Removed successfully
|
||||
toast:
|
||||
update: update success
|
||||
update_password: Password changed successfully.
|
||||
|
@ -725,7 +975,14 @@ ui:
|
|||
title: Related Questions
|
||||
btn: Add question
|
||||
answers: answers
|
||||
invite_to_answer:
|
||||
title: People asked
|
||||
desc: Invite people who you think might know the answer.
|
||||
invite: Invite to answer
|
||||
add: Add people
|
||||
search: Search people
|
||||
question_detail:
|
||||
action: Action
|
||||
Asked: Asked
|
||||
asked: asked
|
||||
update: Modified
|
||||
|
@ -733,9 +990,15 @@ ui:
|
|||
Views: Viewed
|
||||
Follow: Follow
|
||||
Following: Following
|
||||
follow_tip: Follow this question to receive notifications
|
||||
answered: answered
|
||||
closed_in: Closed in
|
||||
show_exist: Show existing question.
|
||||
useful: Useful
|
||||
question_useful: It is useful and clear
|
||||
question_un_useful: It is unclear or not useful
|
||||
answer_useful: It is useful
|
||||
answer_un_useful: It is not useful
|
||||
answers:
|
||||
title: Answers
|
||||
score: Score
|
||||
|
@ -752,10 +1015,20 @@ 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.
|
||||
tips:
|
||||
header_1: Thanks for your answer
|
||||
li1_1: Please be sure to <strong>answer the question</strong>. Provide details and share your research.
|
||||
li1_2: Back up any statements you make with references or personal experience.
|
||||
header_2: But <strong>avoid</strong> ...
|
||||
li2_1: Asking for help, seeking clarification, or responding to other answers.
|
||||
reopen:
|
||||
confirm_btn: Reopen
|
||||
title: Reopen this post
|
||||
content: Are you sure you want to reopen?
|
||||
success: This post has been reopened
|
||||
pin:
|
||||
title: Pin this post
|
||||
content: Are you sure you wish to pinned globally? This post will appear at the top of all post lists.
|
||||
confirm_btn: Pin
|
||||
delete:
|
||||
title: Delete this post
|
||||
question: >-
|
||||
|
@ -763,11 +1036,11 @@ ui:
|
|||
answer_accepted: >-
|
||||
<p>We do not recommend <strong>deleting accepted answer</strong> because doing so deprives future readers of this knowledge. </p> Repeated deletion of accepted answers can result in your account being blocked from answering. Are you sure you wish to delete?
|
||||
other: Are you sure you wish to delete?
|
||||
tip_question_deleted: This post has been deleted
|
||||
tip_answer_deleted: This answer has been deleted
|
||||
btns:
|
||||
confirm: Confirm
|
||||
cancel: Cancel
|
||||
edit: Edit
|
||||
save: Save
|
||||
delete: Delete
|
||||
login: Log in
|
||||
|
@ -778,6 +1051,12 @@ ui:
|
|||
approve: Approve
|
||||
reject: Reject
|
||||
skip: Skip
|
||||
discard_draft: Discard draft
|
||||
pinned: Pinned
|
||||
all: All
|
||||
question: Question
|
||||
answer: Answer
|
||||
comment: Comment
|
||||
search:
|
||||
title: Search Results
|
||||
keywords: Keywords
|
||||
|
@ -812,7 +1091,6 @@ ui:
|
|||
modal_confirm:
|
||||
title: Error...
|
||||
account_result:
|
||||
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: >-
|
||||
|
@ -873,8 +1151,7 @@ ui:
|
|||
accepted: Accepted
|
||||
answered: answered
|
||||
asked: asked
|
||||
upvote: upvote
|
||||
downvote: downvote
|
||||
downvoted: downvoted
|
||||
mod_short: Mod
|
||||
mod_long: Moderators
|
||||
x_reputation: reputation
|
||||
|
@ -936,6 +1213,7 @@ ui:
|
|||
admin_name:
|
||||
label: Name
|
||||
msg: Name cannot be empty.
|
||||
character: 'Must use the character set "a-z", "0-9", " - . _"'
|
||||
admin_password:
|
||||
label: Password
|
||||
text: >-
|
||||
|
@ -966,11 +1244,11 @@ ui:
|
|||
votes: votes
|
||||
answers: answers
|
||||
accepted: Accepted
|
||||
page_404:
|
||||
desc: "Unfortunately, this page doesn't exist."
|
||||
back_home: Back to homepage
|
||||
page_50X:
|
||||
desc: The server encountered an error and could not complete your request.
|
||||
page_error:
|
||||
http_error: HTTP Error {{ code }}
|
||||
desc_403: You don’t have permission to access this page.
|
||||
desc_404: Unfortunately, this page doesn't exist.
|
||||
desc_50X: The server encountered an error and could not complete your request.
|
||||
back_home: Back to homepage
|
||||
page_maintenance:
|
||||
desc: "We are under maintenance, we'll be back soon."
|
||||
|
@ -993,8 +1271,16 @@ ui:
|
|||
seo: SEO
|
||||
customize: Customize
|
||||
themes: Themes
|
||||
css-html: CSS/HTML
|
||||
css_html: CSS/HTML
|
||||
login: Login
|
||||
privileges: Privileges
|
||||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
@ -1037,6 +1323,7 @@ ui:
|
|||
pending: Pending
|
||||
completed: Completed
|
||||
flagged: Flagged
|
||||
flagged_type: Flagged {{ type }}
|
||||
created: Created
|
||||
action: Action
|
||||
review: Review
|
||||
|
@ -1064,7 +1351,7 @@ ui:
|
|||
closed_name: closed
|
||||
closed_desc: "A closed question can't answer, but still can edit, vote and comment."
|
||||
deleted_name: deleted
|
||||
deleted_desc: All reputation gained and lost will be restored.
|
||||
deleted_desc: This post will be deleted.
|
||||
btn_cancel: Cancel
|
||||
btn_submit: Submit
|
||||
btn_next: Next
|
||||
|
@ -1163,11 +1450,11 @@ ui:
|
|||
validate: Please enter a valid URL.
|
||||
text: The address of your site.
|
||||
short_desc:
|
||||
label: Short Site Description (optional)
|
||||
label: Short Site Description
|
||||
msg: Short site description cannot be empty.
|
||||
text: "Short description, as used in the title tag on homepage."
|
||||
desc:
|
||||
label: Site Description (optional)
|
||||
label: Site Description
|
||||
msg: Site description cannot be empty.
|
||||
text: "Describe this site in one sentence, as used in the meta description tag."
|
||||
contact_email:
|
||||
|
@ -1177,14 +1464,6 @@ ui:
|
|||
text: Email address of key contact responsible for this site.
|
||||
interface:
|
||||
page_title: Interface
|
||||
logo:
|
||||
label: Logo (optional)
|
||||
msg: Site logo cannot be empty.
|
||||
text: You can upload your image or <1>reset</1> it to the site title text.
|
||||
theme:
|
||||
label: Theme
|
||||
msg: Theme cannot be empty.
|
||||
text: Select an existing theme.
|
||||
language:
|
||||
label: Interface Language
|
||||
msg: Interface language cannot be empty.
|
||||
|
@ -1236,18 +1515,18 @@ ui:
|
|||
branding:
|
||||
page_title: Branding
|
||||
logo:
|
||||
label: Logo (optional)
|
||||
label: Logo
|
||||
msg: Logo cannot be empty.
|
||||
text: The logo image at the top left of your site. Use a wide rectangular image with a height of 56 and an aspect ratio greater than 3:1. If left blank, the site title text will be shown.
|
||||
mobile_logo:
|
||||
label: Mobile Logo (optional)
|
||||
label: Mobile Logo
|
||||
text: The logo used on mobile version of your site. Use a wide rectangular image with a height of 56. If left blank, the image from the "logo" setting will be used.
|
||||
square_icon:
|
||||
label: Square Icon (optional)
|
||||
label: Square Icon
|
||||
msg: Square icon cannot be empty.
|
||||
text: Image used as the base for metadata icons. Should ideally be larger than 512x512.
|
||||
favicon:
|
||||
label: Favicon (optional)
|
||||
label: Favicon
|
||||
text: A favicon for your site. To work correctly over a CDN it must be a png. Will be resized to 32x32. If left blank, "square icon" will be used.
|
||||
legal:
|
||||
page_title: Legal
|
||||
|
@ -1302,21 +1581,77 @@ ui:
|
|||
footer:
|
||||
label: Footer
|
||||
text: This will insert before </html>.
|
||||
sidebar:
|
||||
label: Sidebar
|
||||
text: This will insert in sidebar.
|
||||
login:
|
||||
page_title: Login
|
||||
membership:
|
||||
title: Membership
|
||||
label: Allow new registrations
|
||||
text: Turn off to prevent anyone from creating a new account.
|
||||
email_registration:
|
||||
title: Email registration
|
||||
label: Allow email registration
|
||||
text: Turn off to prevent anyone creating new account through email.
|
||||
allowed_email_domains:
|
||||
title: Allowed email domains
|
||||
text: Email domains that users must register accounts with. One domain per line. Ignored when empty.
|
||||
private:
|
||||
title: Private
|
||||
label: Login required
|
||||
text: Only logged in users can access this community.
|
||||
installed_plugins:
|
||||
title: Installed Plugins
|
||||
filter:
|
||||
all: All
|
||||
active: Active
|
||||
inactive: Inactive
|
||||
outdated: Outdated
|
||||
plugins:
|
||||
label: Plugins
|
||||
text: Select an existing plugin.
|
||||
name: Name
|
||||
version: Version
|
||||
status: Status
|
||||
action: Action
|
||||
deactivate: Deactivate
|
||||
activate: Activate
|
||||
settings: Settings
|
||||
settings_users:
|
||||
title: Users
|
||||
avatar:
|
||||
label: Default Avatar
|
||||
text: For users without a custom avatar of their own.
|
||||
gravatar_base_url:
|
||||
label: Gravatar Base URL
|
||||
text: URL of the Gravatar provider’s API base. Ignored when empty.
|
||||
profile_editable:
|
||||
title: Profile Editable
|
||||
allow_update_display_name:
|
||||
label: Allow users to change their display name
|
||||
allow_update_username:
|
||||
label: Allow users to change their username
|
||||
allow_update_avatar:
|
||||
label: Allow users to change their profile image
|
||||
allow_update_bio:
|
||||
label: Allow users to change their about me
|
||||
allow_update_website:
|
||||
label: Allow users to change their website
|
||||
allow_update_location:
|
||||
label: Allow users to change their location
|
||||
privilege:
|
||||
title: Privileges
|
||||
level:
|
||||
label: Reputation required level
|
||||
text: Choose the reputation required for the privileges
|
||||
form:
|
||||
optional: (optional)
|
||||
empty: cannot be empty
|
||||
invalid: is invalid
|
||||
btn_submit: Save
|
||||
not_found_props: "Required property {{ key }} not found."
|
||||
select: Select
|
||||
page_review:
|
||||
review: Review
|
||||
proposed: proposed
|
||||
|
@ -1343,6 +1678,10 @@ ui:
|
|||
closed: closed
|
||||
reopened: reopened
|
||||
created: created
|
||||
pin: pinned
|
||||
unpin: unpinned
|
||||
show: listed
|
||||
hide: unlisted
|
||||
title: "History for"
|
||||
tag_title: "Timeline for"
|
||||
show_votes: "Show votes"
|
||||
|
@ -1357,10 +1696,20 @@ ui:
|
|||
no_data: "We couldn't find anything."
|
||||
users:
|
||||
title: Users
|
||||
users_with_the_most_reputation: Users with the highest reputation scores
|
||||
users_with_the_most_vote: Users who voted the most
|
||||
users_with_the_most_reputation: Users with the highest reputation scores this week
|
||||
users_with_the_most_vote: Users who voted the most this week
|
||||
staffs: Our community staff
|
||||
reputation: reputation
|
||||
votes: votes
|
||||
|
||||
|
||||
prompt:
|
||||
leave_page: Are you sure you want to leave the page?
|
||||
changes_not_save: Your changes may not be saved.
|
||||
draft:
|
||||
discard_confirm: Are you sure you want to discard your draft?
|
||||
messages:
|
||||
post_deleted: This post has been deleted.
|
||||
post_pin: This post has been pinned.
|
||||
post_unpin: This post has been unpinned.
|
||||
post_hide_list: This post has been hidden from list.
|
||||
post_show_list: This post has been shown to list.
|
||||
post_reopen: This post has been reopened.
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: Neue e-mail
|
||||
msg:
|
||||
empty: E-Mail darf nicht leer sein.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Administrator
|
||||
|
|
|
@ -882,6 +882,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1317,13 +1320,11 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
|
||||
|
||||
admin:
|
||||
admin_header:
|
||||
|
|
|
@ -877,6 +877,9 @@ ui:
|
|||
label: Nuevo correo electrónico
|
||||
msg:
|
||||
empty: El correo electrónico no puede estar vacío.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1299,13 +1302,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Administrador
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: Nouvel e-mail
|
||||
msg:
|
||||
empty: L'email ne peut pas être vide.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: Email Baru
|
||||
msg:
|
||||
empty: Email tidak boleh kosong.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: 管理者
|
||||
|
|
2199
i18n/pl_PL.yaml
2199
i18n/pl_PL.yaml
File diff suppressed because it is too large
Load Diff
|
@ -856,6 +856,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: Nový e-mail
|
||||
msg:
|
||||
empty: E-mail nemôže byť prázdny.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Administrátor
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: New Email
|
||||
msg:
|
||||
empty: Email cannot be empty.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: Admin
|
||||
|
|
|
@ -50,21 +50,21 @@ backend:
|
|||
admin:
|
||||
other: 拥有管理网站的全部权限。
|
||||
moderator:
|
||||
other: 拥有访问除后台管理以外的所有权限。
|
||||
other: 拥有除访问后台管理以外的所有权限。
|
||||
privilege:
|
||||
level_1:
|
||||
description:
|
||||
other: 第 1 级 (私人团队、群组所需的声望)
|
||||
other: 级别 1(少量声望要求,适合私有团队、群组)
|
||||
level_2:
|
||||
description:
|
||||
other: 级别2(启动社区所需的低声望)
|
||||
other: 级别 2(低声望要求,适合初启动的社区)
|
||||
level_3:
|
||||
description:
|
||||
other: 等级 3 (成熟社区所需的高声望)
|
||||
other: 级别 3(高声望要求,适合成熟的社区)
|
||||
rank_question_add_label:
|
||||
other: 提问s
|
||||
other: 提问
|
||||
rank_answer_add_label:
|
||||
other: 写入答案
|
||||
other: 写答案
|
||||
rank_comment_add_label:
|
||||
other: 发表评论
|
||||
rank_report_add_label:
|
||||
|
@ -86,23 +86,23 @@ backend:
|
|||
rank_tag_add_label:
|
||||
other: 创建新标签
|
||||
rank_tag_edit_label:
|
||||
other: 编辑标签描述(需要审核)
|
||||
other: 编辑标签描述(需要审核)
|
||||
rank_question_edit_label:
|
||||
other: 编辑对方的问题 (需要审查)
|
||||
other: 编辑别人的问题(需要审核)
|
||||
rank_answer_edit_label:
|
||||
other: 编辑对方的答案 (需要审查)
|
||||
other: 编辑别人的答案(需要审核)
|
||||
rank_question_edit_without_review_label:
|
||||
other: 不经评论编辑对方的问题
|
||||
other: 编辑别人的问题无需审核
|
||||
rank_answer_edit_without_review_label:
|
||||
other: 编辑对方的答案而不需要审核
|
||||
other: 编辑别人的答案无需审核
|
||||
rank_question_audit_label:
|
||||
other: 审查问题
|
||||
other: 审核问题编辑
|
||||
rank_answer_audit_label:
|
||||
other: 审核回答
|
||||
other: 审核回答编辑
|
||||
rank_tag_audit_label:
|
||||
other: 审核标签编辑
|
||||
rank_tag_edit_without_review_label:
|
||||
other: 编辑标签且无需审核
|
||||
other: 编辑标签无需审核
|
||||
rank_tag_synonym_label:
|
||||
other: 管理标签同义词
|
||||
email:
|
||||
|
@ -856,6 +856,9 @@ ui:
|
|||
label: 新邮箱
|
||||
msg:
|
||||
empty: 邮箱不能为空
|
||||
oauth:
|
||||
connect: 连接到 {{ auth_name }}
|
||||
remove: 移除 {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: 向你的账户添加恢复邮件地址。
|
||||
btn_update: 更新电子邮件地址
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: 插件
|
||||
installed_plugins: 已安装插件
|
||||
website_welcome: 欢迎来到 {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: 登录
|
||||
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码并登录。
|
||||
login_failed_email_tip: 登录失败,请允许此应用访问您的邮箱信息,然后重试。
|
||||
oauth:
|
||||
connect: 连接到 {{ auth_name }}
|
||||
remove: 移除 {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: 后台管理
|
||||
|
|
|
@ -856,6 +856,9 @@ ui:
|
|||
label: 新電子郵件
|
||||
msg:
|
||||
empty: 郵箱不能為空
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
oauth_bind_email:
|
||||
subtitle: Add a recovery email to your account.
|
||||
btn_update: Update email address
|
||||
|
@ -1274,13 +1277,10 @@ ui:
|
|||
plugins: Plugins
|
||||
installed_plugins: Installed Plugins
|
||||
website_welcome: Welcome to {{site_name}}
|
||||
plugins:
|
||||
user_center:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
||||
oauth:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
||||
admin:
|
||||
admin_header:
|
||||
title: 後台管理
|
||||
|
|
|
@ -2,8 +2,9 @@ package cli
|
|||
|
||||
import (
|
||||
"bytes"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
|
@ -101,8 +102,10 @@ func BuildNewAnswer(outputPath string, plugins []string, originalAnswerInfo Orig
|
|||
builder := newAnswerBuilder(outputPath, plugins, originalAnswerInfo)
|
||||
builder.DoTask(createMainGoFile)
|
||||
builder.DoTask(downloadGoModFile)
|
||||
builder.DoTask(copyUIFiles)
|
||||
builder.DoTask(overwriteIndexTs)
|
||||
builder.DoTask(buildUI)
|
||||
builder.DoTask(mergeI18nFiles)
|
||||
builder.DoTask(replaceNecessaryFile)
|
||||
builder.DoTask(buildBinary)
|
||||
builder.DoTask(cleanByproduct)
|
||||
return builder.BuildError
|
||||
|
@ -120,6 +123,7 @@ func formatPlugins(plugins []string) (formatted []*pluginInfo) {
|
|||
return formatted
|
||||
}
|
||||
|
||||
// createMainGoFile creates main.go file in tmp dir that content is mainGoTpl
|
||||
func createMainGoFile(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("[build] tmp dir: %s\n", b.tmpDir)
|
||||
err = dir.CreateDirIfNotExist(b.tmpDir)
|
||||
|
@ -169,6 +173,7 @@ func createMainGoFile(b *buildingMaterial) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// downloadGoModFile run go mod commands to download dependencies
|
||||
func downloadGoModFile(b *buildingMaterial) (err error) {
|
||||
// If user specify a module replacement, use it. Otherwise, use the latest version.
|
||||
if len(b.answerModuleReplacement) > 0 {
|
||||
|
@ -191,6 +196,81 @@ func downloadGoModFile(b *buildingMaterial) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// copyUIFiles copy ui files from answer module to tmp dir
|
||||
func copyUIFiles(b *buildingMaterial) (err error) {
|
||||
goListCmd := b.newExecCmd("go", "list", "-mod=mod", "-m", "-f", "{{.Dir}}", "github.com/answerdev/answer")
|
||||
buf := new(bytes.Buffer)
|
||||
goListCmd.Stdout = buf
|
||||
if err = goListCmd.Run(); err != nil {
|
||||
return fmt.Errorf("failed to run go list: %w", err)
|
||||
}
|
||||
|
||||
goModUIDir := filepath.Join(strings.TrimSpace(buf.String()), "ui")
|
||||
localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui/")
|
||||
if err = copyDirEntries(os.DirFS(goModUIDir), ".", localUIBuildDir); err != nil {
|
||||
return fmt.Errorf("failed to copy ui files: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// overwriteIndexTs overwrites index.ts file in ui/src/plugins/ dir
|
||||
func overwriteIndexTs(b *buildingMaterial) (err error) {
|
||||
localUIPluginDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui/src/plugins/")
|
||||
|
||||
folders, err := getFolders(localUIPluginDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get folders: %w", err)
|
||||
}
|
||||
|
||||
content := generateIndexTsContent(folders)
|
||||
err = os.WriteFile(filepath.Join(localUIPluginDir, "index.ts"), []byte(content), 0644)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write index.ts: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFolders(dir string) ([]string, error) {
|
||||
var folders []string
|
||||
files, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, file := range files {
|
||||
if file.IsDir() && file.Name() != "builtin" {
|
||||
folders = append(folders, file.Name())
|
||||
}
|
||||
}
|
||||
return folders, nil
|
||||
}
|
||||
|
||||
func generateIndexTsContent(folders []string) string {
|
||||
builder := &strings.Builder{}
|
||||
builder.WriteString("export default null;\n\n")
|
||||
for _, folder := range folders {
|
||||
builder.WriteString(fmt.Sprintf("export { default as %s } from './%s';\n", folder, folder))
|
||||
}
|
||||
return builder.String()
|
||||
}
|
||||
|
||||
// buildUI run pnpm install and pnpm build commands to build ui
|
||||
func buildUI(b *buildingMaterial) (err error) {
|
||||
localUIBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui")
|
||||
|
||||
pnpmInstallCmd := b.newExecCmd("pnpm", "install")
|
||||
pnpmInstallCmd.Dir = localUIBuildDir
|
||||
if err = pnpmInstallCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
pnpmBuildCmd := b.newExecCmd("pnpm", "build")
|
||||
pnpmBuildCmd.Dir = localUIBuildDir
|
||||
if err = pnpmBuildCmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func replaceNecessaryFile(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("try to replace ui build directory\n")
|
||||
uiBuildDir := filepath.Join(b.tmpDir, "vendor/github.com/answerdev/answer/ui")
|
||||
|
@ -198,6 +278,7 @@ func replaceNecessaryFile(b *buildingMaterial) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
// mergeI18nFiles merge i18n files
|
||||
func mergeI18nFiles(b *buildingMaterial) (err error) {
|
||||
fmt.Printf("try to merge i18n files\n")
|
||||
|
||||
|
@ -285,37 +366,60 @@ func mergeI18nFiles(b *buildingMaterial) (err error) {
|
|||
return err
|
||||
}
|
||||
|
||||
func copyDirEntries(sourceFs embed.FS, sourceDir string, targetDir string) (err error) {
|
||||
entries, err := ui.Build.ReadDir(sourceDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
func copyDirEntries(sourceFs fs.FS, sourceDir string, targetDir string) (err error) {
|
||||
err = dir.CreateDirIfNotExist(targetDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
err = copyDirEntries(sourceFs, filepath.Join(sourceDir, entry.Name()), filepath.Join(targetDir, entry.Name()))
|
||||
err = fs.WalkDir(sourceFs, sourceDir, func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Convert the path to use forward slashes, important because we use embedded FS which always uses forward slashes
|
||||
path = filepath.ToSlash(path)
|
||||
|
||||
// Construct the absolute path for the source file/directory
|
||||
srcPath := filepath.Join(sourceDir, path)
|
||||
|
||||
// Construct the absolute path for the destination file/directory
|
||||
dstPath := filepath.Join(targetDir, path)
|
||||
|
||||
if d.IsDir() {
|
||||
// Create the directory in the destination
|
||||
err := os.MkdirAll(dstPath, os.ModePerm)
|
||||
if err != nil {
|
||||
return err
|
||||
return fmt.Errorf("failed to create directory %s: %w", dstPath, err)
|
||||
}
|
||||
} else {
|
||||
// Open the source file
|
||||
srcFile, err := sourceFs.Open(srcPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open source file %s: %w", srcPath, err)
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
// Create the destination file
|
||||
dstFile, err := os.Create(dstPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create destination file %s: %w", dstPath, err)
|
||||
}
|
||||
defer dstFile.Close()
|
||||
|
||||
// Copy the file contents
|
||||
_, err = io.Copy(dstFile, srcFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to copy file contents from %s to %s: %w", srcPath, dstPath, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
file, err := sourceFs.ReadFile(filepath.Join(sourceDir, entry.Name()))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
filename := filepath.Join(targetDir, entry.Name())
|
||||
err = os.WriteFile(filename, file, 0666)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// buildBinary build binary file
|
||||
func buildBinary(b *buildingMaterial) (err error) {
|
||||
versionInfo := b.originalAnswerInfo
|
||||
cmdPkg := "github.com/answerdev/answer/cmd"
|
||||
|
@ -329,6 +433,7 @@ func buildBinary(b *buildingMaterial) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
// cleanByproduct delete tmp dir
|
||||
func cleanByproduct(b *buildingMaterial) (err error) {
|
||||
return os.RemoveAll(b.tmpDir)
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/segmentfault/pacman/log"
|
||||
|
@ -25,6 +26,11 @@ func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
|
|||
log.Error("get site general failed:", err)
|
||||
return
|
||||
}
|
||||
siteInfo, err := t.siteInfoService.GetSiteSeo(ctx)
|
||||
if err != nil {
|
||||
log.Error("get site GetSiteSeo failed:", err)
|
||||
return
|
||||
}
|
||||
|
||||
sitemapInfo := &schema.SiteMapList{}
|
||||
infoStr, err := t.data.Cache.GetString(ctx, schema.SitemapCachekey)
|
||||
|
@ -32,6 +38,10 @@ func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
|
|||
log.Errorf("get Cache failed: %s", err)
|
||||
return
|
||||
}
|
||||
hasTitle := false
|
||||
if siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitle || siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID {
|
||||
hasTitle = true
|
||||
}
|
||||
if err = json.Unmarshal([]byte(infoStr), sitemapInfo); err != nil {
|
||||
log.Errorf("get sitemap info failed: %s", err)
|
||||
return
|
||||
|
@ -45,6 +55,7 @@ func (t *TemplateRenderController) Sitemap(ctx *gin.Context) {
|
|||
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
|
||||
"list": sitemapInfo.QuestionIDs,
|
||||
"general": general,
|
||||
"hastitle": hasTitle,
|
||||
},
|
||||
)
|
||||
} else {
|
||||
|
@ -68,6 +79,15 @@ func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error
|
|||
log.Error("get site general failed:", err)
|
||||
return err
|
||||
}
|
||||
siteInfo, err := t.siteInfoService.GetSiteSeo(ctx)
|
||||
if err != nil {
|
||||
log.Error("get site GetSiteSeo failed:", err)
|
||||
return err
|
||||
}
|
||||
hasTitle := false
|
||||
if siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitle || siteInfo.PermaLink == constant.PermaLinkQuestionIDAndTitleByShortID {
|
||||
hasTitle = true
|
||||
}
|
||||
|
||||
cachekey := fmt.Sprintf(schema.SitemapPageCachekey, page)
|
||||
infoStr, err := t.data.Cache.GetString(ctx, cachekey)
|
||||
|
@ -85,6 +105,7 @@ func (t *TemplateRenderController) SitemapPage(ctx *gin.Context, page int) error
|
|||
"xmlHeader": template.HTML(`<?xml version="1.0" encoding="UTF-8"?>`),
|
||||
"list": sitemapInfo.PageData,
|
||||
"general": general,
|
||||
"hastitle": hasTitle,
|
||||
},
|
||||
)
|
||||
return nil
|
||||
|
|
|
@ -15,13 +15,13 @@ import (
|
|||
|
||||
func updateCount(x *xorm.Engine) error {
|
||||
fns := []func(*xorm.Engine) error{
|
||||
inviteAnswer,
|
||||
addPrivilegeForInviteSomeoneToAnswer,
|
||||
addGravatarBaseURL,
|
||||
updateQuestionCount,
|
||||
updateTagCount,
|
||||
updateUserQuestionCount,
|
||||
updateUserAnswerCount,
|
||||
inviteAnswer,
|
||||
inBoxData,
|
||||
}
|
||||
for _, fn := range fns {
|
||||
|
@ -224,7 +224,7 @@ func updateTagCount(x *xorm.Engine) error {
|
|||
}
|
||||
} else {
|
||||
tag.QuestionCount = 0
|
||||
if _, err = x.Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
|
||||
if _, err = x.Cols("question_count").Update(tag, &entity.Tag{ID: tag.ID}); err != nil {
|
||||
log.Errorf("update %+v tag failed: %s", tag.ID, err)
|
||||
return fmt.Errorf("update tag failed: %w", err)
|
||||
}
|
||||
|
|
|
@ -10,6 +10,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/entity"
|
||||
collectioncommon "github.com/answerdev/answer/internal/service/collection_common"
|
||||
"github.com/answerdev/answer/internal/service/unique"
|
||||
"github.com/answerdev/answer/pkg/uid"
|
||||
"github.com/segmentfault/pacman/errors"
|
||||
"xorm.io/xorm"
|
||||
)
|
||||
|
@ -148,6 +149,9 @@ func (cr *collectionRepo) GetCollectionPage(ctx context.Context, page, pageSize
|
|||
// SearchObjectCollected check object is collected or not
|
||||
func (cr *collectionRepo) SearchObjectCollected(ctx context.Context, userID string, objectIds []string) (map[string]bool, error) {
|
||||
collectedMap := make(map[string]bool)
|
||||
for k, object_id := range objectIds {
|
||||
objectIds[k] = uid.DeShortID(object_id)
|
||||
}
|
||||
list, err := cr.SearchByObjectIDsAndUser(ctx, userID, objectIds)
|
||||
if err != nil {
|
||||
err = errors.InternalServer(reason.DatabaseError).WithError(err).WithStack()
|
||||
|
|
|
@ -566,6 +566,7 @@ func (as *AnswerService) SearchFormatInfo(ctx context.Context, answers []*entity
|
|||
}
|
||||
|
||||
for _, item := range list {
|
||||
item.ID = uid.EnShortID(item.ID)
|
||||
item.MemberActions = permission.GetAnswerPermission(ctx, req.UserID, item.UserID, req.CanEdit, req.CanDelete)
|
||||
}
|
||||
return list, nil
|
||||
|
|
|
@ -3,9 +3,12 @@ package questioncommon
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/answerdev/answer/internal/base/constant"
|
||||
"github.com/answerdev/answer/internal/base/data"
|
||||
"github.com/answerdev/answer/internal/base/handler"
|
||||
"github.com/answerdev/answer/internal/base/reason"
|
||||
"github.com/answerdev/answer/internal/service/activity_common"
|
||||
|
@ -65,6 +68,7 @@ type QuestionCommon struct {
|
|||
metaService *meta.MetaService
|
||||
configService *config.ConfigService
|
||||
activityQueueService activity_queue.ActivityQueueService
|
||||
data *data.Data
|
||||
}
|
||||
|
||||
func NewQuestionCommon(questionRepo QuestionRepo,
|
||||
|
@ -78,6 +82,7 @@ func NewQuestionCommon(questionRepo QuestionRepo,
|
|||
metaService *meta.MetaService,
|
||||
configService *config.ConfigService,
|
||||
activityQueueService activity_queue.ActivityQueueService,
|
||||
data *data.Data,
|
||||
) *QuestionCommon {
|
||||
return &QuestionCommon{
|
||||
questionRepo: questionRepo,
|
||||
|
@ -91,6 +96,7 @@ func NewQuestionCommon(questionRepo QuestionRepo,
|
|||
metaService: metaService,
|
||||
configService: configService,
|
||||
activityQueueService: activityQueueService,
|
||||
data: data,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -546,6 +552,57 @@ func (as *QuestionCommon) RemoveAnswer(ctx context.Context, id string) (err erro
|
|||
return as.answerRepo.RemoveAnswer(ctx, id)
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) SitemapCron(ctx context.Context) {
|
||||
data := &schema.SiteMapList{}
|
||||
questionNum, err := qs.questionRepo.GetQuestionCount(ctx)
|
||||
if err != nil {
|
||||
log.Error("GetQuestionCount error", err)
|
||||
return
|
||||
}
|
||||
if questionNum <= schema.SitemapMaxSize {
|
||||
questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, 0, int(questionNum))
|
||||
if err != nil {
|
||||
log.Error("GetQuestionIDsPage error", err)
|
||||
return
|
||||
}
|
||||
data.QuestionIDs = questionIDList
|
||||
|
||||
} else {
|
||||
nums := make([]int, 0)
|
||||
totalpages := int(math.Ceil(float64(questionNum) / float64(schema.SitemapMaxSize)))
|
||||
for i := 1; i <= totalpages; i++ {
|
||||
siteMapPagedata := &schema.SiteMapPageList{}
|
||||
nums = append(nums, i)
|
||||
questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, i, int(schema.SitemapMaxSize))
|
||||
if err != nil {
|
||||
log.Error("GetQuestionIDsPage error", err)
|
||||
return
|
||||
}
|
||||
siteMapPagedata.PageData = questionIDList
|
||||
if setCacheErr := qs.SetCache(ctx, fmt.Sprintf(schema.SitemapPageCachekey, i), siteMapPagedata); setCacheErr != nil {
|
||||
log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
|
||||
}
|
||||
}
|
||||
data.MaxPageNum = nums
|
||||
}
|
||||
if setCacheErr := qs.SetCache(ctx, schema.SitemapCachekey, data); setCacheErr != nil {
|
||||
log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) SetCache(ctx context.Context, cachekey string, info interface{}) error {
|
||||
infoStr, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
|
||||
err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashBoardCacheTime)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (qs *QuestionCommon) ShowListFormat(ctx context.Context, data *entity.Question) *schema.QuestionInfo {
|
||||
return qs.ShowFormat(ctx, data)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package service
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
|
@ -595,13 +594,16 @@ func (qs *QuestionService) UpdateQuestionInviteUser(ctx context.Context, req *sc
|
|||
//send notification
|
||||
oldInviteUserIDsStr := originQuestion.InviteUserID
|
||||
oldInviteUserIDs := make([]string, 0)
|
||||
needSendNotificationUserIDs := make([]string, 0)
|
||||
if oldInviteUserIDsStr != "" {
|
||||
err = json.Unmarshal([]byte(oldInviteUserIDsStr), &oldInviteUserIDs)
|
||||
if err == nil {
|
||||
needSendNotificationUserIDs := converter.ArrayNotInArray(oldInviteUserIDs, inviteUserIDs)
|
||||
go qs.notificationInviteUser(ctx, needSendNotificationUserIDs, originQuestion.ID, originQuestion.Title, req.UserID)
|
||||
needSendNotificationUserIDs = converter.ArrayNotInArray(oldInviteUserIDs, inviteUserIDs)
|
||||
}
|
||||
} else {
|
||||
needSendNotificationUserIDs = inviteUserIDs
|
||||
}
|
||||
go qs.notificationInviteUser(ctx, needSendNotificationUserIDs, originQuestion.ID, originQuestion.Title, req.UserID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1353,52 +1355,5 @@ func (qs *QuestionService) changeQuestionToRevision(ctx context.Context, questio
|
|||
}
|
||||
|
||||
func (qs *QuestionService) SitemapCron(ctx context.Context) {
|
||||
data := &schema.SiteMapList{}
|
||||
questionNum, err := qs.questionRepo.GetQuestionCount(ctx)
|
||||
if err != nil {
|
||||
log.Error("GetQuestionCount error", err)
|
||||
return
|
||||
}
|
||||
if questionNum <= schema.SitemapMaxSize {
|
||||
questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, 0, int(questionNum))
|
||||
if err != nil {
|
||||
log.Error("GetQuestionIDsPage error", err)
|
||||
return
|
||||
}
|
||||
data.QuestionIDs = questionIDList
|
||||
|
||||
} else {
|
||||
nums := make([]int, 0)
|
||||
totalpages := int(math.Ceil(float64(questionNum) / float64(schema.SitemapMaxSize)))
|
||||
for i := 1; i <= totalpages; i++ {
|
||||
siteMapPagedata := &schema.SiteMapPageList{}
|
||||
nums = append(nums, i)
|
||||
questionIDList, err := qs.questionRepo.GetQuestionIDsPage(ctx, i, int(schema.SitemapMaxSize))
|
||||
if err != nil {
|
||||
log.Error("GetQuestionIDsPage error", err)
|
||||
return
|
||||
}
|
||||
siteMapPagedata.PageData = questionIDList
|
||||
if setCacheErr := qs.SetCache(ctx, fmt.Sprintf(schema.SitemapPageCachekey, i), siteMapPagedata); setCacheErr != nil {
|
||||
log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
|
||||
}
|
||||
}
|
||||
data.MaxPageNum = nums
|
||||
}
|
||||
if setCacheErr := qs.SetCache(ctx, schema.SitemapCachekey, data); setCacheErr != nil {
|
||||
log.Errorf("set sitemap cron SetCache failed: %s", setCacheErr)
|
||||
}
|
||||
}
|
||||
|
||||
func (qs *QuestionService) SetCache(ctx context.Context, cachekey string, info interface{}) error {
|
||||
infoStr, err := json.Marshal(info)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
|
||||
err = qs.data.Cache.SetString(ctx, cachekey, string(infoStr), schema.DashBoardCacheTime)
|
||||
if err != nil {
|
||||
return errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
|
||||
}
|
||||
return nil
|
||||
qs.questioncommon.SitemapCron(ctx)
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ import (
|
|||
"github.com/answerdev/answer/internal/schema"
|
||||
"github.com/answerdev/answer/internal/service/config"
|
||||
"github.com/answerdev/answer/internal/service/export"
|
||||
questioncommon "github.com/answerdev/answer/internal/service/question_common"
|
||||
"github.com/answerdev/answer/internal/service/siteinfo_common"
|
||||
tagcommon "github.com/answerdev/answer/internal/service/tag_common"
|
||||
"github.com/answerdev/answer/plugin"
|
||||
|
@ -27,6 +28,7 @@ type SiteInfoService struct {
|
|||
emailService *export.EmailService
|
||||
tagCommonService *tagcommon.TagCommonService
|
||||
configService *config.ConfigService
|
||||
questioncommon *questioncommon.QuestionCommon
|
||||
}
|
||||
|
||||
func NewSiteInfoService(
|
||||
|
@ -35,6 +37,8 @@ func NewSiteInfoService(
|
|||
emailService *export.EmailService,
|
||||
tagCommonService *tagcommon.TagCommonService,
|
||||
configService *config.ConfigService,
|
||||
questioncommon *questioncommon.QuestionCommon,
|
||||
|
||||
) *SiteInfoService {
|
||||
plugin.RegisterGetSiteURLFunc(func() string {
|
||||
generalSiteInfo, err := siteInfoCommonService.GetSiteGeneral(context.Background())
|
||||
|
@ -51,6 +55,7 @@ func NewSiteInfoService(
|
|||
emailService: emailService,
|
||||
tagCommonService: tagCommonService,
|
||||
configService: configService,
|
||||
questioncommon: questioncommon,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,7 +19,7 @@ echo "cmd is "$cmd
|
|||
$cmd
|
||||
if [ ! -f "./new_answer" ]; then
|
||||
echo "new_answer is not exist build failed"
|
||||
exit 0
|
||||
exit 1
|
||||
fi
|
||||
rm answer
|
||||
mv new_answer answer
|
||||
|
|
|
@ -31,3 +31,8 @@ yarn.lock
|
|||
package-lock.json
|
||||
.eslintcache
|
||||
/.vscode/
|
||||
|
||||
/* !/src/plugins
|
||||
/src/plugins/*
|
||||
!/src/plugins/builtin
|
||||
!/src/plugins/Demo
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
"build": "react-app-rewired build",
|
||||
"lint": "eslint . --cache --fix --ext .ts,.tsx",
|
||||
"prettier": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,scss,md}\"",
|
||||
"prepare": "cd .. && husky install",
|
||||
"preinstall": "node ./scripts/preinstall.js",
|
||||
"pre-commit": "lint-staged"
|
||||
},
|
||||
|
|
|
@ -560,13 +560,10 @@ export interface OauthBindEmailReq {
|
|||
must: boolean;
|
||||
}
|
||||
|
||||
export interface OauthConnectorItem {
|
||||
export interface UserOauthConnectorItem {
|
||||
icon: string;
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export interface UserOauthConnectorItem extends OauthConnectorItem {
|
||||
binding: boolean;
|
||||
external_id: string;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
import { FC, ReactNode, memo } from 'react';
|
||||
|
||||
import builtin from '@/plugins/builtin';
|
||||
import * as plugins from '@/plugins';
|
||||
import { Plugin, PluginType } from '@/utils/pluginKit';
|
||||
|
||||
/**
|
||||
* Note:Please set at least either of the `slug_name` and `type` attributes, otherwise no plugins will be rendered.
|
||||
*
|
||||
* @field slug_name: The `slug_name` of the plugin needs to be rendered.
|
||||
* If this property is set, `PluginRender` will use it first (regardless of whether `type` is set)
|
||||
* to find the corresponding plugin and render it.
|
||||
* @field type: Used to formulate the rendering of all plugins of this type.
|
||||
* (if the `slug_name` attribute is set, it will be ignored)
|
||||
* @field prop: Any attribute you want to configure, e.g. `className`
|
||||
*/
|
||||
|
||||
interface Props {
|
||||
slug_name?: string;
|
||||
type?: PluginType;
|
||||
children?: ReactNode;
|
||||
[prop: string]: any;
|
||||
}
|
||||
|
||||
const findPlugin: (s, k: 'slug_name' | 'type', v) => Plugin[] = (
|
||||
source,
|
||||
k,
|
||||
v,
|
||||
) => {
|
||||
const ret: Plugin[] = [];
|
||||
if (source) {
|
||||
Object.keys(source).forEach((i) => {
|
||||
const p = source[i];
|
||||
if (p && p.component && p.info && p.info[k] === v) {
|
||||
ret.push(p);
|
||||
}
|
||||
});
|
||||
}
|
||||
return ret;
|
||||
};
|
||||
|
||||
const Index: FC<Props> = ({ slug_name, type, children, ...props }) => {
|
||||
const fk = slug_name ? 'slug_name' : 'type';
|
||||
const fv = fk === 'slug_name' ? slug_name : type;
|
||||
const bp = findPlugin(builtin, fk, fv);
|
||||
const vp = findPlugin(plugins, fk, fv);
|
||||
const pluginSlice = [...bp, ...vp];
|
||||
|
||||
if (!pluginSlice.length) {
|
||||
return null;
|
||||
}
|
||||
/**
|
||||
* TODO: Rendering control for non-builtin plug-ins
|
||||
* ps: Logic such as version compatibility determination can be placed here
|
||||
*/
|
||||
|
||||
return (
|
||||
<>
|
||||
{pluginSlice.map((ps) => {
|
||||
const PluginFC = ps.component;
|
||||
return (
|
||||
// @ts-ignore
|
||||
<PluginFC key={ps.info.slug_name} {...props}>
|
||||
{children}
|
||||
</PluginFC>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
|
@ -1,6 +1,3 @@
|
|||
---
|
||||
sidebar_position: 0
|
||||
---
|
||||
# Schema Form
|
||||
|
||||
## Introduction
|
||||
|
@ -12,7 +9,7 @@ A React component capable of building HTML forms out of a [JSON schema](https://
|
|||
```tsx
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { SchemaForm, initFormData, JSONSchema, UISchema } from '@/components';
|
||||
import { SchemaForm, initFormData, JSONSchema, UISchema, FormKit } from '@/components';
|
||||
|
||||
const schema: JSONSchema = {
|
||||
title: 'General',
|
||||
|
@ -51,6 +48,14 @@ const uiSchema: UISchema = {
|
|||
|
||||
const Form = () => {
|
||||
const [formData, setFormData] = useState(initFormData(schema));
|
||||
|
||||
const formRef = useRef<{
|
||||
validator: () => Promise<boolean>;
|
||||
}>(null);
|
||||
|
||||
const refreshConfig: FormKit['refreshConfig'] = async () => {
|
||||
// refreshFormConfig();
|
||||
};
|
||||
|
||||
const handleChange = (data) => {
|
||||
setFormData(data);
|
||||
|
@ -58,27 +63,56 @@ const Form = () => {
|
|||
|
||||
return (
|
||||
<SchemaForm
|
||||
ref={formRef}
|
||||
schema={schema}
|
||||
uiSchema={uiSchema}
|
||||
formData={formData}
|
||||
onChange={handleChange}
|
||||
refreshConfig={refreshConfig}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Form;
|
||||
|
||||
```
|
||||
|
||||
## Props
|
||||
---
|
||||
|
||||
| Property | Description | Type | Default |
|
||||
| -------- | ---------------------------------------- | ------------------------------------- | ------- |
|
||||
| schema | Describe the form structure with schema | [JSONSchema](#json-schema) | - |
|
||||
| uiSchema | Describe the properties of the field | [UISchema](#uischema) | - |
|
||||
| formData | Describe form data | [FormData](#formdata) | - |
|
||||
| onChange | Callback function when form data changes | (data: [FormData](#formdata)) => void | - |
|
||||
| onSubmit | Callback function when form is submitted | (data: React.FormEvent) => void | - |
|
||||
## Form Props
|
||||
|
||||
```ts
|
||||
interface FormProps {
|
||||
// Describe the form structure with schema
|
||||
schema: JSONSchema | null;
|
||||
// Describe the properties of the field
|
||||
uiSchema?: UISchema;
|
||||
// Describe form data
|
||||
formData: Type.FormDataType | null;
|
||||
// Callback function when form data changes
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
// Handler for when a form fires a `submit` event
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
/**
|
||||
* Callback method for updating form configuration
|
||||
* information (schema/uiSchema) in UIAction
|
||||
*/
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
}
|
||||
```
|
||||
|
||||
## Form Ref
|
||||
|
||||
```ts
|
||||
export interface FormRef {
|
||||
validator: () => Promise<boolean>;
|
||||
}
|
||||
```
|
||||
|
||||
When you need to validate a form and get the result outside the form, you can create a `FormRef` with `useRef` and pass it to the form using the `ref` property.
|
||||
|
||||
This allows you to validate the form and get the result outside the form using `formRef.current.validator()`.
|
||||
|
||||
---
|
||||
|
||||
## Types Definition
|
||||
### JSONSchema
|
||||
|
@ -102,24 +136,70 @@ export interface JSONSchema {
|
|||
}
|
||||
```
|
||||
|
||||
### UIOptions
|
||||
|
||||
### UISchema
|
||||
```ts
|
||||
export interface UIOptions {
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?: UIOptions;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### UIWidget
|
||||
```ts
|
||||
export type UIWidget =
|
||||
| 'textarea'
|
||||
| 'input'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch'
|
||||
| 'legend'
|
||||
| 'button';
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### UIOptions
|
||||
```ts
|
||||
export type UIOptions =
|
||||
| InputOptions
|
||||
| SelectOptions
|
||||
| UploadOptions
|
||||
| SwitchOptions
|
||||
| TimezoneOptions
|
||||
| CheckboxOptions
|
||||
| RadioOptions
|
||||
| TextareaOptions
|
||||
| ButtonOptions;
|
||||
```
|
||||
|
||||
#### BaseUIOptions
|
||||
```ts
|
||||
export interface BaseUIOptions {
|
||||
empty?: string;
|
||||
className?: string | string[];
|
||||
// Will be appended to the className of the form component itself
|
||||
className?: classnames.Argument;
|
||||
// The className that will be attached to a form field container
|
||||
fieldClassName?: classnames.Argument;
|
||||
// Make a form component render into simplified mode
|
||||
readOnly?: boolean;
|
||||
simplify?: boolean;
|
||||
validator?: (
|
||||
value,
|
||||
formData?,
|
||||
) => Promise<string | true | void> | true | string;
|
||||
}
|
||||
```
|
||||
### InputOptions
|
||||
|
||||
#### InputOptions
|
||||
```ts
|
||||
export interface InputOptions extends UIOptions {
|
||||
export interface InputOptions extends BaseUIOptions {
|
||||
placeholder?: string;
|
||||
type?:
|
||||
inputType?:
|
||||
| 'color'
|
||||
| 'date'
|
||||
| 'datetime-local'
|
||||
|
@ -136,82 +216,88 @@ export interface InputOptions extends UIOptions {
|
|||
| 'week';
|
||||
}
|
||||
```
|
||||
### SelectOptions
|
||||
|
||||
#### SelectOptions
|
||||
```ts
|
||||
export interface SelectOptions extends UIOptions {}
|
||||
```
|
||||
### UploadOptions
|
||||
|
||||
#### UploadOptions
|
||||
```ts
|
||||
export interface UploadOptions extends UIOptions {
|
||||
export interface UploadOptions extends BaseUIOptions {
|
||||
acceptType?: string;
|
||||
imageType?: 'post' | 'avatar' | 'branding';
|
||||
imageType?: Type.UploadType;
|
||||
}
|
||||
```
|
||||
### SwitchOptions
|
||||
|
||||
#### SwitchOptions
|
||||
```ts
|
||||
export interface SwitchOptions extends UIOptions {}
|
||||
export interface SwitchOptions extends BaseUIOptions {
|
||||
label?: string;
|
||||
}
|
||||
```
|
||||
### TimezoneOptions
|
||||
|
||||
#### TimezoneOptions
|
||||
```ts
|
||||
export interface TimezoneOptions extends UIOptions {
|
||||
placeholder?: string;
|
||||
}
|
||||
```
|
||||
### CheckboxOptions
|
||||
|
||||
#### CheckboxOptions
|
||||
```ts
|
||||
export interface CheckboxOptions extends UIOptions {}
|
||||
```
|
||||
### RadioOptions
|
||||
|
||||
#### RadioOptions
|
||||
```ts
|
||||
export interface RadioOptions extends UIOptions {}
|
||||
```
|
||||
### TextareaOptions
|
||||
|
||||
#### TextareaOptions
|
||||
```ts
|
||||
export interface TextareaOptions extends UIOptions {
|
||||
placeholder?: string;
|
||||
rows?: number;
|
||||
}
|
||||
```
|
||||
### UIWidget
|
||||
|
||||
#### ButtonOptions
|
||||
```ts
|
||||
export type UIWidget =
|
||||
| 'textarea'
|
||||
| 'input'
|
||||
| 'checkbox'
|
||||
| 'radio'
|
||||
| 'select'
|
||||
| 'upload'
|
||||
| 'timezone'
|
||||
| 'switch';
|
||||
export interface ButtonOptions extends BaseUIOptions {
|
||||
text: string;
|
||||
icon?: string;
|
||||
action?: UIAction;
|
||||
variant?: ButtonProps['variant'];
|
||||
size?: ButtonProps['size'];
|
||||
}
|
||||
```
|
||||
|
||||
### UISchema
|
||||
|
||||
#### UIAction
|
||||
```ts
|
||||
export interface UISchema {
|
||||
[key: string]: {
|
||||
'ui:widget'?: UIWidget;
|
||||
'ui:options'?:
|
||||
| InputOptions
|
||||
| SelectOptions
|
||||
| UploadOptions
|
||||
| SwitchOptions
|
||||
| TimezoneOptions
|
||||
| CheckboxOptions
|
||||
| RadioOptions
|
||||
| TextareaOptions;
|
||||
export interface UIAction {
|
||||
url: string;
|
||||
method?: 'get' | 'post' | 'put' | 'delete';
|
||||
loading?: {
|
||||
text: string;
|
||||
state?: 'none' | 'pending' | 'completed';
|
||||
};
|
||||
on_complete?: {
|
||||
toast_return_message?: boolean;
|
||||
refresh_form_config?: boolean;
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
#### FormKit
|
||||
```ts
|
||||
export interface FormKit {
|
||||
refreshConfig(): void;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### FormData
|
||||
```ts
|
||||
export interface FormValue<T = any> {
|
||||
|
@ -226,6 +312,44 @@ export interface FormDataType {
|
|||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Backend API
|
||||
|
||||
For backend generating modal form you can return json like this.
|
||||
|
||||
### Response
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "string",
|
||||
"slug_name": "string",
|
||||
"description": "string",
|
||||
"version": "string",
|
||||
"config_fields": [
|
||||
{
|
||||
"name": "string",
|
||||
"type": "textarea",
|
||||
"title": "string",
|
||||
"description": "string",
|
||||
"required": true,
|
||||
"value": "string",
|
||||
"ui_options": {
|
||||
"placeholder": "placeholder",
|
||||
"rows": 4
|
||||
},
|
||||
"options": [
|
||||
{
|
||||
"value": "string",
|
||||
"label": "string"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
## reference
|
||||
|
||||
- [json schema](https://json-schema.org/understanding-json-schema/index.html)
|
||||
|
|
|
@ -12,7 +12,13 @@ import classnames from 'classnames';
|
|||
|
||||
import type * as Type from '@/common/interface';
|
||||
|
||||
import type { JSONSchema, UISchema, BaseUIOptions, FormKit } from './types';
|
||||
import type {
|
||||
JSONSchema,
|
||||
FormProps,
|
||||
FormRef,
|
||||
BaseUIOptions,
|
||||
FormKit,
|
||||
} from './types';
|
||||
import {
|
||||
Legend,
|
||||
Select,
|
||||
|
@ -27,20 +33,6 @@ import {
|
|||
|
||||
export * from './types';
|
||||
|
||||
interface IProps {
|
||||
schema: JSONSchema | null;
|
||||
formData: Type.FormDataType | null;
|
||||
uiSchema?: UISchema;
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
hiddenSubmit?: boolean;
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
interface IRef {
|
||||
validator: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* - [!] Standardised `Admin/Plugins/Config/index.tsx` method for generating dynamic form configurations.
|
||||
|
@ -60,7 +52,7 @@ interface IRef {
|
|||
* @param onChange change event
|
||||
* @param onSubmit submit event
|
||||
*/
|
||||
const SchemaForm: ForwardRefRenderFunction<IRef, IProps> = (
|
||||
const SchemaForm: ForwardRefRenderFunction<FormRef, FormProps> = (
|
||||
{
|
||||
schema,
|
||||
uiSchema = {},
|
||||
|
|
|
@ -1,9 +1,24 @@
|
|||
import { ButtonProps } from 'react-bootstrap';
|
||||
import React from 'react';
|
||||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import * as Type from '@/common/interface';
|
||||
|
||||
export interface FormProps {
|
||||
schema: JSONSchema | null;
|
||||
uiSchema?: UISchema;
|
||||
formData: Type.FormDataType | null;
|
||||
refreshConfig?: FormKit['refreshConfig'];
|
||||
hiddenSubmit?: boolean;
|
||||
onChange?: (data: Type.FormDataType) => void;
|
||||
onSubmit?: (e: React.FormEvent) => void;
|
||||
}
|
||||
|
||||
export interface FormRef {
|
||||
validator: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export interface JSONSchema {
|
||||
title: string;
|
||||
description?: string;
|
||||
|
|
|
@ -41,6 +41,7 @@ import HttpErrorContent from './HttpErrorContent';
|
|||
import CustomSidebar from './CustomSidebar';
|
||||
import ImgViewer from './ImgViewer';
|
||||
import SideNav from './SideNav';
|
||||
import PluginRender from './PluginRender';
|
||||
|
||||
export {
|
||||
Avatar,
|
||||
|
@ -88,5 +89,6 @@ export {
|
|||
CustomSidebar,
|
||||
ImgViewer,
|
||||
SideNav,
|
||||
PluginRender,
|
||||
};
|
||||
export type { EditorRef, JSONSchema, UISchema };
|
||||
|
|
|
@ -4,22 +4,42 @@ import i18next from 'i18next';
|
|||
import en_US from '@i18n/en_US.yaml';
|
||||
import zh_CN from '@i18n/zh_CN.yaml';
|
||||
|
||||
import { DEFAULT_LANG } from '@/common/constants';
|
||||
import { DEFAULT_LANG, LANG_RESOURCE_STORAGE_KEY } from '@/common/constants';
|
||||
import Storage from '@/utils/storage';
|
||||
|
||||
/**
|
||||
* Prevent i18n from re-initialising when the page is refreshed and switching to `fallbackLng`.
|
||||
*/
|
||||
const initLng = i18next.resolvedLanguage || DEFAULT_LANG;
|
||||
const initResources = {
|
||||
en_US: {
|
||||
translation: en_US.ui,
|
||||
},
|
||||
zh_CN: {
|
||||
translation: zh_CN.ui,
|
||||
},
|
||||
};
|
||||
|
||||
const storageLang = Storage.get(LANG_RESOURCE_STORAGE_KEY);
|
||||
if (
|
||||
storageLang &&
|
||||
storageLang.resources &&
|
||||
storageLang.lng &&
|
||||
storageLang.lng !== 'en_US' &&
|
||||
storageLang.lng !== 'zh_CN'
|
||||
) {
|
||||
initResources[storageLang.lng] = {
|
||||
translation: storageLang.resources,
|
||||
};
|
||||
}
|
||||
|
||||
i18next
|
||||
// pass the i18n instance to react-i18next.
|
||||
.use(initReactI18next)
|
||||
.init({
|
||||
resources: {
|
||||
en_US: {
|
||||
translation: en_US.ui,
|
||||
},
|
||||
zh_CN: {
|
||||
translation: zh_CN.ui,
|
||||
},
|
||||
},
|
||||
// debug: process.env.NODE_ENV === 'development',
|
||||
fallbackLng: process.env.REACT_APP_LANG || DEFAULT_LANG,
|
||||
resources: initResources,
|
||||
lng: initLng,
|
||||
fallbackLng: DEFAULT_LANG,
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
|
|
|
@ -13,7 +13,7 @@ import { getLoginConf, checkLoginResult } from './service';
|
|||
|
||||
let checkTimer: NodeJS.Timeout;
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'user_center' });
|
||||
const navigate = useNavigate();
|
||||
const ucAgent = userCenterStore().agent;
|
||||
const agentName = ucAgent?.agent_info?.name || '';
|
||||
|
|
|
@ -28,7 +28,7 @@ const data = [
|
|||
];
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins' });
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'user_center' });
|
||||
const ucAgent = userCenterStore().agent;
|
||||
return (
|
||||
<Col lg={4} className="mx-auto mt-3 py-5">
|
||||
|
|
|
@ -9,8 +9,7 @@ import type {
|
|||
ImgCodeRes,
|
||||
FormDataType,
|
||||
} from '@/common/interface';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth, PluginUcLogin } from '@/plugins';
|
||||
import { Unactivate, WelcomeTitle, PluginRender } from '@/components';
|
||||
import {
|
||||
loggedUserInfoStore,
|
||||
loginSettingStore,
|
||||
|
@ -172,15 +171,16 @@ const Index: React.FC = () => {
|
|||
usePageTags({
|
||||
title: t('login', { keyPrefix: 'page_title' }),
|
||||
});
|
||||
|
||||
return (
|
||||
<Container style={{ paddingTop: '4rem', paddingBottom: '5rem' }}>
|
||||
<WelcomeTitle />
|
||||
{step === 1 ? (
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
{ucAgentInfo ? (
|
||||
<PluginUcLogin className="mb-5" />
|
||||
<PluginRender slug_name="uc_login" className="mb-5" />
|
||||
) : (
|
||||
<PluginOauth className="mb-5" />
|
||||
<PluginRender type="Connector" className="mb-5" />
|
||||
)}
|
||||
{canOriginalLogin ? (
|
||||
<>
|
||||
|
|
|
@ -3,8 +3,7 @@ import { Container, Col } from 'react-bootstrap';
|
|||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { usePageTags } from '@/hooks';
|
||||
import { Unactivate, WelcomeTitle } from '@/components';
|
||||
import { PluginOauth } from '@/plugins';
|
||||
import { Unactivate, WelcomeTitle, PluginRender } from '@/components';
|
||||
import { guard } from '@/utils';
|
||||
|
||||
import SignUpForm from './components/SignUpForm';
|
||||
|
@ -27,7 +26,7 @@ const Index: React.FC = () => {
|
|||
|
||||
{showForm ? (
|
||||
<Col className="mx-auto" md={6} lg={4} xl={3}>
|
||||
<PluginOauth className="mb-5" />
|
||||
<PluginRender type="Connector" className="mb-5" />
|
||||
<SignUpForm callback={onStep} />
|
||||
</Col>
|
||||
) : (
|
||||
|
|
|
@ -18,7 +18,7 @@ const Index = () => {
|
|||
});
|
||||
|
||||
const { t: t2 } = useTranslation('translation', {
|
||||
keyPrefix: 'plugins.oauth',
|
||||
keyPrefix: 'oauth',
|
||||
});
|
||||
|
||||
const deleteLogins = (e, item) => {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
package demo
|
||||
|
||||
import "github.com/answerdev/answer/plugin"
|
||||
|
||||
type DemoPlugin struct {
|
||||
}
|
||||
|
||||
func init() {
|
||||
plugin.Register(&DemoPlugin{})
|
||||
}
|
||||
|
||||
func (d DemoPlugin) Info() plugin.Info {
|
||||
return plugin.Info{
|
||||
Name: plugin.MakeTranslator("i18n.demo.name"),
|
||||
SlugName: "demo_plugin",
|
||||
Description: plugin.MakeTranslator("i18n.demo.description"),
|
||||
Author: "answerdev",
|
||||
Version: "0.0.1",
|
||||
}
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
plugin:
|
||||
ui_plugin_demo:
|
||||
ui:
|
||||
msg: UI Plugin Demo
|
|
@ -0,0 +1,9 @@
|
|||
import pluginKit from '@/utils/pluginKit';
|
||||
|
||||
import en_US from './en_US.yaml';
|
||||
import zh_CN from './zh_CN.yaml';
|
||||
|
||||
pluginKit.initI18nResource({
|
||||
en_US,
|
||||
zh_CN,
|
||||
});
|
|
@ -0,0 +1,4 @@
|
|||
plugin:
|
||||
ui_plugin_demo:
|
||||
ui:
|
||||
msg: UI 插件示例
|
|
@ -0,0 +1,24 @@
|
|||
import { memo, FC } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Alert } from 'react-bootstrap';
|
||||
|
||||
import pluginKit, { PluginInfo } from '@/utils/pluginKit';
|
||||
import './i18n';
|
||||
|
||||
import info from './info.yaml';
|
||||
|
||||
const pluginInfo: PluginInfo = {
|
||||
slug_name: info.slug_name,
|
||||
};
|
||||
|
||||
const Index: FC = () => {
|
||||
const { t } = useTranslation(pluginKit.getTransNs(), {
|
||||
keyPrefix: pluginKit.getTransKeyPrefix(pluginInfo),
|
||||
});
|
||||
|
||||
return <Alert variant="info">{t('msg')}</Alert>;
|
||||
};
|
||||
export default {
|
||||
info: pluginInfo,
|
||||
component: memo(Index),
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
slug_name: ui_plugin_demo
|
||||
version: 0.0.1
|
||||
author: Answer.dev
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
plugin:
|
||||
connector:
|
||||
ui:
|
||||
connect: Connect with {{ auth_name }}
|
||||
remove: Remove {{ auth_name }}
|
|
@ -0,0 +1,9 @@
|
|||
import pluginKit from '@/utils/pluginKit';
|
||||
|
||||
import en_US from './en_US.yaml';
|
||||
import zh_CN from './zh_CN.yaml';
|
||||
|
||||
pluginKit.initI18nResource({
|
||||
en_US,
|
||||
zh_CN,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
plugin:
|
||||
connector:
|
||||
ui:
|
||||
connect: 连接到 {{ auth_name }}
|
||||
remove: 解绑 {{ auth_name }}
|
||||
|
|
@ -4,14 +4,25 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import { useGetStartUseOauthConnector } from '@/services';
|
||||
import pluginKit, { PluginInfo } from '@/utils/pluginKit';
|
||||
import { SvgIcon } from '@/components';
|
||||
|
||||
import info from './info.yaml';
|
||||
import { useGetStartUseOauthConnector } from './services';
|
||||
import './i18n';
|
||||
|
||||
const pluginInfo: PluginInfo = {
|
||||
slug_name: info.slug_name,
|
||||
type: info.type,
|
||||
};
|
||||
interface Props {
|
||||
className?: string;
|
||||
}
|
||||
const Index: FC<Props> = ({ className }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
|
||||
const { t } = useTranslation(pluginKit.getTransNs(), {
|
||||
keyPrefix: pluginKit.getTransKeyPrefix(pluginInfo),
|
||||
});
|
||||
|
||||
const { data } = useGetStartUseOauthConnector();
|
||||
|
||||
if (!data?.length) return null;
|
||||
|
@ -29,4 +40,7 @@ const Index: FC<Props> = ({ className }) => {
|
|||
);
|
||||
};
|
||||
|
||||
export default memo(Index);
|
||||
export default {
|
||||
info: pluginInfo,
|
||||
component: memo(Index),
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
slug_name: connector
|
||||
type: Connector
|
||||
version: 0.0.1
|
||||
link: https://github.com/answerdev/plugins/tree/main/connector/
|
||||
author: Answer.dev
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import useSWR from 'swr';
|
||||
|
||||
import request from '@/utils/request';
|
||||
|
||||
export interface OauthConnectorItem {
|
||||
icon: string;
|
||||
name: string;
|
||||
link: string;
|
||||
}
|
||||
|
||||
export const useGetStartUseOauthConnector = () => {
|
||||
const { data, error } = useSWR<OauthConnectorItem[]>(
|
||||
'/answer/api/v1/connector/info',
|
||||
request.instance.get,
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
};
|
||||
};
|
|
@ -0,0 +1,6 @@
|
|||
plugin:
|
||||
uc_login:
|
||||
ui:
|
||||
login: Login
|
||||
qrcode_login_tip: Please use {{ agentName }} to scan the QR code and log in.
|
||||
login_failed_email_tip: Login failed, please allow this app to access your email information before try again.
|
|
@ -0,0 +1,9 @@
|
|||
import pluginKit from '@/utils/pluginKit';
|
||||
|
||||
import en_US from './en_US.yaml';
|
||||
import zh_CN from './zh_CN.yaml';
|
||||
|
||||
pluginKit.initI18nResource({
|
||||
en_US,
|
||||
zh_CN,
|
||||
});
|
|
@ -0,0 +1,6 @@
|
|||
plugin:
|
||||
uc_login:
|
||||
ui:
|
||||
login: 登录
|
||||
qrcode_login_tip: 请使用 {{ agentName }} 扫描二维码登录
|
||||
login_failed_email_tip: 登录失败, 请允许该应用程序访问您的电子邮件信息,然后再试一次。
|
|
@ -4,14 +4,25 @@ import { useTranslation } from 'react-i18next';
|
|||
|
||||
import classnames from 'classnames';
|
||||
|
||||
import pluginKit, { PluginInfo } from '@/utils/pluginKit';
|
||||
import { SvgIcon } from '@/components';
|
||||
import { userCenterStore } from '@/stores';
|
||||
import './i18n';
|
||||
|
||||
import info from './info.yaml';
|
||||
|
||||
interface Props {
|
||||
className?: classnames.Argument;
|
||||
}
|
||||
|
||||
const pluginInfo: PluginInfo = {
|
||||
slug_name: info.slug_name,
|
||||
};
|
||||
|
||||
const Index: FC<Props> = ({ className }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'plugins.oauth' });
|
||||
const { t } = useTranslation(pluginKit.getTransNs(), {
|
||||
keyPrefix: pluginKit.getTransKeyPrefix(pluginInfo),
|
||||
});
|
||||
const ucAgent = userCenterStore().agent;
|
||||
const ucLoginRedirect =
|
||||
ucAgent?.enabled && ucAgent?.agent_info?.login_redirect_url;
|
||||
|
@ -31,5 +42,7 @@ const Index: FC<Props> = ({ className }) => {
|
|||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export default memo(Index);
|
||||
export default {
|
||||
info: pluginInfo,
|
||||
component: memo(Index),
|
||||
};
|
|
@ -0,0 +1,4 @@
|
|||
slug_name: uc_login
|
||||
version: 0.0.1
|
||||
author: Answer.dev
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import Connector from './Connector';
|
||||
import UcLogin from './UcLogin';
|
||||
|
||||
export default {
|
||||
Connector,
|
||||
UcLogin,
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
import PluginOauth from './PluginOauth';
|
||||
import PluginUcLogin from './PluginUcLogin';
|
||||
export default null;
|
||||
|
||||
export { PluginOauth, PluginUcLogin };
|
||||
// export { default as Demo } from './Demo';
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface PluginOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
type: UIWidget;
|
||||
title: string;
|
||||
description: string;
|
||||
ui_options?: UIOptions;
|
||||
options?: PluginOption[];
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
slug_name: string;
|
||||
config_fields: PluginItem[];
|
||||
}
|
|
@ -2,7 +2,29 @@ import qs from 'qs';
|
|||
import useSWR from 'swr';
|
||||
|
||||
import request from '@/utils/request';
|
||||
import type { PluginConfig } from '@/plugins/types';
|
||||
import { UIOptions, UIWidget } from '@/components/SchemaForm';
|
||||
|
||||
export interface PluginOption {
|
||||
label: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface PluginItem {
|
||||
name: string;
|
||||
type: UIWidget;
|
||||
title: string;
|
||||
description: string;
|
||||
ui_options?: UIOptions;
|
||||
options?: PluginOption[];
|
||||
value?: string;
|
||||
required?: boolean;
|
||||
}
|
||||
|
||||
export interface PluginConfig {
|
||||
name: string;
|
||||
slug_name: string;
|
||||
config_fields: PluginItem[];
|
||||
}
|
||||
|
||||
export const useQueryPlugins = (params) => {
|
||||
const apiUrl = `/answer/admin/api/plugins?${qs.stringify(params)}`;
|
||||
|
|
|
@ -23,14 +23,3 @@ export const useOauthConnectorInfoByUser = () => {
|
|||
export const userOauthUnbind = (data: { external_id: string }) => {
|
||||
return request.delete('/answer/api/v1/connector/user/unbinding', data);
|
||||
};
|
||||
|
||||
export const useGetStartUseOauthConnector = () => {
|
||||
const { data, error } = useSWR<Type.OauthConnectorItem[]>(
|
||||
'/answer/api/v1/connector/info',
|
||||
request.instance.get,
|
||||
);
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
};
|
||||
};
|
||||
|
|
|
@ -17,6 +17,9 @@ import {
|
|||
|
||||
import Storage from './storage';
|
||||
|
||||
/**
|
||||
* localize kit for i18n
|
||||
*/
|
||||
export const loadLanguageOptions = async (forAdmin = false) => {
|
||||
const languageOptions = forAdmin
|
||||
? await getAdminLanguageOptions()
|
||||
|
@ -68,13 +71,6 @@ const addI18nResource = async (langName) => {
|
|||
}
|
||||
};
|
||||
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const localeDayjs = (langName) => {
|
||||
langName = langName.replace('_', '-').toLowerCase();
|
||||
dayjs.locale(langName);
|
||||
};
|
||||
|
||||
export const getCurrentLang = () => {
|
||||
const loggedUser = loggedUserInfoStore.getState().user;
|
||||
const adminInterface = interfaceStore.getState().interface;
|
||||
|
@ -88,6 +84,16 @@ export const getCurrentLang = () => {
|
|||
return currentLang;
|
||||
};
|
||||
|
||||
/**
|
||||
* localize for Day.js
|
||||
*/
|
||||
dayjs.extend(utc);
|
||||
dayjs.extend(timezone);
|
||||
const localeDayjs = (langName) => {
|
||||
langName = langName.replace('_', '-').toLowerCase();
|
||||
dayjs.locale(langName);
|
||||
};
|
||||
|
||||
export const setupAppLanguage = async () => {
|
||||
const lang = getCurrentLang();
|
||||
if (!i18next.getDataByLanguage(lang)) {
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
import { NamedExoticComponent, FC } from 'react';
|
||||
|
||||
import i18next from 'i18next';
|
||||
|
||||
/**
|
||||
* This information is to be defined for all components.
|
||||
* It may be used for feature upgrades or version compatibility processing.
|
||||
*
|
||||
* @field slug_name: Unique identity string for the plugin, usually configured in `info.yaml`
|
||||
* @field type: The type of plugin is defined and a single type of plugin can have multiple implementations.
|
||||
* For example, a plugin of type `Connector` can have a `google` implementation and a `github` implementation.
|
||||
* `PluginRender` automatically renders the plug-in types already included in `PluginType`.
|
||||
* @field name: Plugin name, optionally configurable. Usually read from the `i18n` file
|
||||
* @field description: Plugin description, optionally configurable. Usually read from the `i18n` file
|
||||
*/
|
||||
|
||||
const I18N_NS = 'plugin';
|
||||
|
||||
export type PluginType = 'Connector';
|
||||
export interface PluginInfo {
|
||||
slug_name: string;
|
||||
type?: PluginType;
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface Plugin {
|
||||
info: PluginInfo;
|
||||
component: NamedExoticComponent | FC;
|
||||
}
|
||||
|
||||
interface I18nResource {
|
||||
[lng: string]: {
|
||||
plugin: {
|
||||
[slug_name: string]: {
|
||||
ui: any;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
const addResourceBundle = (resource: I18nResource) => {
|
||||
if (resource) {
|
||||
Object.keys(resource).forEach((lng) => {
|
||||
const r = resource[lng];
|
||||
|
||||
i18next.addResourceBundle(lng, I18N_NS, r.plugin, true, true);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const initI18nResource = (resource: I18nResource) => {
|
||||
addResourceBundle(resource);
|
||||
/**
|
||||
* Note: In development mode,
|
||||
* when the base i18n file is changed, `i18next` will reinitialise the updated resource file,
|
||||
* which will cause the resource package added in the plugin to be lost
|
||||
* and will need to be automatically re-added by listening for events
|
||||
*/
|
||||
i18next.on('initialized', () => {
|
||||
addResourceBundle(resource);
|
||||
});
|
||||
};
|
||||
|
||||
const getTransNs = () => {
|
||||
return I18N_NS;
|
||||
};
|
||||
|
||||
const getTransKeyPrefix = (info: PluginInfo) => {
|
||||
const kp = `${info.slug_name}.ui`;
|
||||
return kp;
|
||||
};
|
||||
|
||||
export default {
|
||||
initI18nResource,
|
||||
getTransNs,
|
||||
getTransKeyPrefix,
|
||||
};
|
|
@ -2,7 +2,11 @@
|
|||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
{{ range .list }}
|
||||
<url>
|
||||
{{if $.hastitle}}
|
||||
<loc>{{$.general.SiteUrl}}/questions/{{.ID}}/{{.Title}}</loc>
|
||||
{{else}}
|
||||
<loc>{{$.general.SiteUrl}}/questions/{{.ID}}</loc>
|
||||
{{end}}
|
||||
<lastmod>{{.UpdateTime}}</lastmod>
|
||||
</url>
|
||||
{{ end }}
|
||||
|
|
Loading…
Reference in New Issue