diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index 85e0b99e..e640b642 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1173,6 +1173,7 @@ ui: post_lowercase: post filter: Filter ignore: Ignore + submit: Submit search: title: Search Results keywords: Keywords @@ -1678,8 +1679,8 @@ ui: page_title: Write restrict_answer: title: Restrict answer - label: Each user can only write one answer for each question - text: "They can use the edit link to refine and improve their existing answer, instead." + label: Each user can only write one answer for the same question + text: "Turn off to allow users to write multiple answers to the same question, which may cause answers to be unfocused." recommend_tags: label: Recommend tags text: "Please input tag slug above, one tag per line." @@ -1814,16 +1815,16 @@ ui: edit_tag: Edit tag empty: No review tasks left. approve_this_type: Do you approve this {{ type }}? - suggest_type_edit: Suggest {{ type }} edit - flag_type: Flag {{ type }} + suggest_edits: Suggested edits + flag_post: Flag post + flag_user: Flag user filter_label: Type - queued_post: Queued post - queued_user: Queued user - flagged_post: Flagged post - flagged_user: Flagged user - suggested_post_edit: Suggested post edit - suggested_tag_edit: Suggested tag edit reputation: reputation + flag_post_type: Flagged this post as {{ type }} + flag_user_type: Flagged this user as {{ type }} + edit_post: Edit post + list_post: List post + unlist_post: Unlist post timeline: undeleted: undeleted deleted: deleted diff --git a/i18n/zh_CN.yaml b/i18n/zh_CN.yaml index f204860a..1151de96 100644 --- a/i18n/zh_CN.yaml +++ b/i18n/zh_CN.yaml @@ -1628,8 +1628,8 @@ ui: page_title: 编辑 restrict_answer: title: 限制一个回答 - label: 每个用户对于每个问题只能有一个回答 - text: "用户可以使用编辑按钮优化已有的回答" + label: 每个用户只能为同一问题写一个回答 + text: "关闭以允许用户对同一问题编写多个回答,这可能会导致回答不集中。" recommend_tags: label: 推荐标签 text: "请在上方输入标签固定链接,每行一个标签。" diff --git a/ui/src/common/constants.ts b/ui/src/common/constants.ts index 8868c9a0..d11dc7f8 100644 --- a/ui/src/common/constants.ts +++ b/ui/src/common/constants.ts @@ -66,9 +66,9 @@ export const ADMIN_LIST_STATUS = { variant: 'text-bg-danger', name: 'deleted', }, - unlisted: { + unlist: { variant: 'text-bg-secondary', - name: 'unlisted', + name: 'unlist', }, }; diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 9c0b777d..847c4fb7 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -562,7 +562,7 @@ export interface TimelineRes { timeline: TimelineItem[]; } -export interface ReviewItem { +export interface SuggestReviewItem { type: 'question' | 'answer' | 'tag'; info: { url_title?: string; @@ -584,9 +584,43 @@ export interface ReviewItem { content: Tag | QuestionDetailRes | AnswerItem; }; } -export interface ReviewResp { +export interface SuggestReviewResp { count: number; - list: ReviewItem[]; + list: SuggestReviewItem[]; +} + +export interface ReasonItem { + content_type: string; + description: string; + name: string; + placeholder: string; + reason_type: number; +} + +export interface FlagReviewItem { + object_type: 'question' | 'answer' | 'comment' | 'user'; + object_id: string; + object_show_status: number; + object_status: number; + tags: Tag[]; + title: string; + original_text: string; + reason: ReasonItem; + author_user_info: UserInfoBase; + submitter_user: UserInfoBase; + created_at: number; + submit_at: number; + comment_id: string; + question_id: string; + answer_id: string; + answer_count: number; + answer_accepted?: boolean; + flag_id: string; +} + +export interface FlagReviewResp { + count: number; + list: FlagReviewItem[]; } export interface UserRoleItem { @@ -638,3 +672,27 @@ export interface UserPluginsConfigRes { name: string; slug_name: string; } + +export interface ReviewTypeItem { + label: string; + name: string; + todo_amount: number; +} + +export interface PutFlagReviewParams { + operation_type: + | 'edit_post' + | 'close_post' + | 'delete_post' + | 'unlist_post' + | 'ignore_report'; + flag_id: string; + close_msg?: string; + close_type?: number; + title?: string; + content?: string; + tags?: Tag[]; + // mention_username_list?: any; + captcha_code?: any; + captcha_id?: any; +} diff --git a/ui/src/hooks/useReportModal/index.tsx b/ui/src/hooks/useReportModal/index.tsx index 9ad66513..24d26b02 100644 --- a/ui/src/hooks/useReportModal/index.tsx +++ b/ui/src/hooks/useReportModal/index.tsx @@ -25,14 +25,20 @@ import ReactDOM from 'react-dom/client'; import { useToast, useCaptchaModal } from '@/hooks'; import type * as Type from '@/common/interface'; -import { reportList, postReport, closeQuestion, putReport } from '@/services'; +import { + reportList, + postReport, + closeQuestion, + putReport, + putFlagReviewAction, +} from '@/services'; interface Params { isBackend?: boolean; type: Type.ReportType; id: string; title?: string; - action: Type.ReportAction; + action: Type.ReportAction | 'flag_review_close'; } const useReportModal = (callback?: () => void) => { @@ -63,6 +69,7 @@ const useReportModal = (callback?: () => void) => { rootRef.current.root = ReactDOM.createRoot(div); }, []); const getList = ({ type, action, isBackend }: Params) => { + // @ts-ignore reportList({ type, action, isBackend }).then((res) => { setList(res); setShow(true); @@ -123,6 +130,19 @@ const useReportModal = (callback?: () => void) => { }); return; } + + if (params.type === 'question' && params.action === 'flag_review_close') { + putFlagReviewAction({ + flag_id: params.id, + operation_type: 'close_post', + close_type: reportType.type, + close_msg: content.value, + }).then(() => { + onClose(); + asyncCallback(); + }); + return; + } if (!params.isBackend && params.action === 'flag') { rCaptcha.check(() => { const flagReq = { diff --git a/ui/src/pages/Admin/Answers/index.tsx b/ui/src/pages/Admin/Answers/index.tsx index 869bec9a..febc8e81 100644 --- a/ui/src/pages/Admin/Answers/index.tsx +++ b/ui/src/pages/Admin/Answers/index.tsx @@ -118,13 +118,13 @@ const Answers: FC = () => { className="text-break text-wrap" rel="noreferrer"> {li.question_info.title} + {li.accepted === 2 && ( + + )} - {li.accepted === 2 && ( - - )}
{ - {t(ADMIN_LIST_STATUS.unlisted.name)} + {t(ADMIN_LIST_STATUS.unlist.name)} )} diff --git a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx index bda9112a..2f79305b 100644 --- a/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx +++ b/ui/src/pages/Questions/Ask/components/SearchQuestion/index.tsx @@ -46,7 +46,7 @@ const SearchQuestion = ({ similarQuestions }) => { @@ -56,7 +56,7 @@ const SearchQuestion = ({ similarQuestions }) => { : null} {item.accepted_answer ? ( - + {t('x_answers', { @@ -67,7 +67,7 @@ const SearchQuestion = ({ similarQuestions }) => { ) : ( item.answer_count > 0 && ( - + {t('x_answers', { diff --git a/ui/src/pages/Review/components/ApproveDropdown/index.tsx b/ui/src/pages/Review/components/ApproveDropdown/index.tsx index 7f0e6bd7..d26b10f6 100644 --- a/ui/src/pages/Review/components/ApproveDropdown/index.tsx +++ b/ui/src/pages/Review/components/ApproveDropdown/index.tsx @@ -1,34 +1,199 @@ -import { useState } from 'react'; +import { FC, useState } from 'react'; import { Dropdown, Button } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; +import { Modal } from '@/components'; +import { putFlagReviewAction } from '@/services'; +import { useCaptchaModal, useReportModal, useToast } from '@/hooks'; +import type * as Type from '@/common/interface'; import EditPostModal from '../EditPostModal'; -const Index = () => { +interface IProps { + itemData: Type.FlagReviewItem | null; + curFilter: string; + objectType: Type.FlagReviewItem['object_type'] | ''; + approveCallback: () => void; +} + +const Index: FC = ({ + itemData, + objectType, + curFilter, + approveCallback, +}) => { + console.log(objectType); + const { t } = useTranslation('translation', { keyPrefix: 'page_review' }); + + const [isLoading, setIsLoading] = useState(false); const [showEditPostModal, setShowEditPostModal] = useState(false); + const closeModal = useReportModal(approveCallback); + const toast = useToast(); + const dCaptcha = useCaptchaModal('delete'); const handleEditPostModalState = () => { setShowEditPostModal(!showEditPostModal); }; + const handleDelete = () => { + let content = ''; + + setIsLoading(true); + + if (objectType === 'question') { + content = + Number(itemData?.answer_count) > 0 + ? t('question', { keyPrefix: 'delete' }) + : t('other', { keyPrefix: 'delete' }); + } + if (objectType === 'answer') { + content = itemData?.answer_accepted + ? t('answer_accepted', { keyPrefix: 'delete' }) + : t('other', { keyPrefix: 'delete' }); + } + if (objectType === 'comment') { + content = t('other', { keyPrefix: 'delete' }); + } + Modal.confirm({ + title: t('title', { keyPrefix: 'delete' }), + content, + cancelBtnVariant: 'link', + confirmBtnVariant: 'danger', + confirmText: t('delete', { keyPrefix: 'btns' }), + onConfirm: () => { + dCaptcha.check(() => { + const req: Type.PutFlagReviewParams = { + operation_type: 'delete_post', + flag_id: String(itemData?.flag_id), + captcha_code: undefined, + captcha_id: undefined, + }; + dCaptcha.resolveCaptchaReq(req); + + delete req.captcha_code; + delete req.captcha_id; + + putFlagReviewAction(req) + .then(async () => { + await dCaptcha.close(); + let msg = ''; + if (objectType === 'question') { + msg = t('post_deleted', { keyPrefix: 'messages' }); + } + if (objectType === 'answer') { + msg = t('tip_answer_deleted'); + } + if (objectType === 'answer' || objectType === 'question') { + toast.onShow({ + msg, + variant: 'success', + }); + } + approveCallback(); + }) + .catch((ex) => { + if (ex.isError) { + dCaptcha.handleCaptchaError(ex.list); + } + }) + .finally(() => { + setIsLoading(false); + }); + }); + }, + }); + }; + + const handleAction = (type) => { + if (type === 'delete') { + handleDelete(); + } + + if (type === 'close') { + closeModal.onShow({ + type: 'question', + id: itemData?.flag_id || '', + action: 'flag_review_close', + }); + } + + if (type === 'unlist') { + const keyPrefix = 'question_detail.unlist'; + Modal.confirm({ + title: t('title', { keyPrefix }), + content: t('content', { keyPrefix }), + cancelBtnVariant: 'link', + confirmText: t('confirm_btn', { keyPrefix }), + onConfirm: () => { + putFlagReviewAction({ + operation_type: 'unlist_post', + flag_id: itemData?.flag_id || '', + }).then(() => { + toast.onShow({ + msg: t(`post_${type}`, { keyPrefix: 'messages' }), + variant: 'success', + }); + approveCallback(); + }); + }, + }); + } + }; + + const handleActionEdit = () => { + handleEditPostModalState(); + }; + return (
- Approve + {t('approve', { keyPrefix: 'btns' })} - Deactivate user - Suspend user - Delete user + handleActionEdit()}> + {t('edit_post')} + + {curFilter === 'normal' && objectType === 'question' && ( + handleAction('close')}> + {t('close', { keyPrefix: 'btns' })} + + )} + {curFilter !== 'deleted' && ( + handleAction('delete')}> + {t('delete', { keyPrefix: 'btns' })} + + )} + {objectType === 'question' && ( + <> + + {itemData?.object_show_status !== 2 && ( + handleAction('unlist')}> + {t('unlist_post')} + + )} + + )}
); diff --git a/ui/src/pages/Review/components/EditPostModal/index.tsx b/ui/src/pages/Review/components/EditPostModal/index.tsx index a07a70cf..e35a82ca 100644 --- a/ui/src/pages/Review/components/EditPostModal/index.tsx +++ b/ui/src/pages/Review/components/EditPostModal/index.tsx @@ -1,20 +1,36 @@ -import { FC, useState } from 'react'; +import { FC, useState, useEffect } from 'react'; import { Modal, Button, Form } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import classNames from 'classnames'; -import { modifyQuestion } from '@/services'; -import { useCaptchaModal } from '@/hooks'; -import { Editor, TagSelector } from '@/components'; -import { handleFormError } from '@/utils'; +import { putFlagReviewAction } from '@/services'; +import { useCaptchaModal, usePageUsers } from '@/hooks'; +import { Editor, TagSelector, Mentions, TextArea } from '@/components'; +import { + // matchedUsers, + parseUserInfo, + handleFormError, + parseEditMentionUser, +} from '@/utils'; import type * as Type from '@/common/interface'; import './index.scss'; interface Props { + originalData: { + id: string; + flag_id: string; + question_id?: string; + answer_id?: string; + title: string; + content: string; + tags: Type.Tag[]; + }; + objectType: Type.FlagReviewItem['object_type'] | ''; visible: boolean; handleClose: () => void; + callback?: () => void; } interface FormDataItem { @@ -23,65 +39,151 @@ interface FormDataItem { content: Type.FormValue; } -const Index: FC = ({ visible = false, handleClose }) => { - const initFormData = { - title: { - value: '', - isInvalid: false, - errorMsg: '', - }, - tags: { - value: [], - isInvalid: false, - errorMsg: '', - }, - content: { - value: '', - isInvalid: false, - errorMsg: '', - }, - }; +const initFormData = { + title: { + value: '', + isInvalid: false, + errorMsg: '', + }, + tags: { + value: [], + isInvalid: false, + errorMsg: '', + }, + content: { + value: '', + isInvalid: false, + errorMsg: '', + }, +}; + +const Index: FC = ({ + originalData, + visible = false, + objectType, + handleClose, + callback, +}) => { const { t } = useTranslation('translation', { keyPrefix: 'ask' }); const [formData, setFormData] = useState(initFormData); const [focusEditor, setFocusEditor] = useState(false); + const [loaded, setLoaded] = useState(false); + const pageUsers = usePageUsers(); const editCaptcha = useCaptchaModal('edit'); + const onClose = (bol) => { + if (bol) { + callback?.(); + } + handleClose(); + setLoaded(false); + }; + const handleInput = (data: Partial) => { + if (!loaded) { + return; + } setFormData({ ...formData, ...data, }); }; - const handleSubmit = async (event: React.FormEvent) => { + const checkValidated = (): boolean => { + let bol = true; + const { title, tags, content } = formData; + if (objectType === 'question') { + if (!title.value) { + bol = false; + formData.title = { + value: title.value, + isInvalid: true, + errorMsg: t('form.fields.title.msg.empty', { + keyPrefix: 'ask', + }), + }; + } + + if (!tags.value.length) { + bol = false; + formData.tags = { + value: tags.value, + isInvalid: true, + errorMsg: t('form.fields.tags.msg.empty', { + keyPrefix: 'ask', + }), + }; + } + } + + if (!content.value || Array.from(content.value.trim()).length < 6) { + bol = false; + formData.content = { + value: content.value, + isInvalid: true, + errorMsg: t('form.fields.answer.feedback.characters', { + keyPrefix: 'edit_answer', + }), + }; + } else { + formData.content = { + value: content.value, + isInvalid: false, + errorMsg: '', + }; + } + + setFormData({ + ...formData, + }); + return bol; + }; + + const handleSubmit = (event: React.FormEvent) => { event.preventDefault(); event.stopPropagation(); - const params: Type.QuestionParams = { + if (!checkValidated()) { + return; + } + + const params: Type.PutFlagReviewParams = { title: formData.title.value, content: formData.content.value, tags: formData.tags.value, + operation_type: 'edit_post', + flag_id: originalData.flag_id, }; + if (objectType === 'answer') { + delete params.title; + delete params.tags; + } + if (objectType === 'comment') { + const { value } = formData.content; + // const users = matchedUsers(value); + // const userNames = unionBy(users.map((user) => user.userName)); + const commentMarkDown = parseUserInfo(value); + + // params.mention_username_list = userNames; + params.content = commentMarkDown; + + delete params.title; + delete params.tags; + } editCaptcha.check(() => { - const ep = { - ...params, - id: '', - edit_summary: '', - }; - const imgCode = editCaptcha.getCaptcha(); - if (imgCode.verify) { - ep.captcha_code = imgCode.captcha_code; - ep.captcha_id = imgCode.captcha_id; + if (objectType === 'question') { + const imgCode = editCaptcha.getCaptcha(); + if (imgCode.verify) { + params.captcha_code = imgCode.captcha_code; + params.captcha_id = imgCode.captcha_id; + } } - modifyQuestion(ep) - .then(async (res) => { + putFlagReviewAction(params) + .then(async () => { await editCaptcha.close(); - console.log('res', res); - // navigate(pathFactory.questionLanding(qid, res?.url_title), { - // state: { isReview: res?.wait_for_review }, - // }); + onClose(true); }) .catch((err) => { if (err.isError) { @@ -92,102 +194,180 @@ const Index: FC = ({ visible = false, handleClose }) => { }); }); }; + + const handleSelected = (val) => { + if (!loaded) { + return; + } + setFormData({ + ...formData, + content: { + value: val, + errorMsg: '', + isInvalid: false, + }, + }); + }; + + useEffect(() => { + if (!visible) { + return; + } + + formData.title.value = originalData.title; + formData.content.value = originalData.content; + formData.tags.value = originalData.tags.map((item) => { + return { + ...item, + parsed_text: '', + original_text: '', + }; + }); + setFormData({ ...formData }); + setLoaded(true); + }, [visible]); + return ( onClose(false)} className="w-100" dialogClassName="edit-post-modal"> - Edit post + + {t('edit_post', { keyPrefix: 'page_review' })} + - -
- - {t('form.fields.title.label')} - { - handleInput({ - title: { - value: e.target.value, - isInvalid: false, - errorMsg: '', - }, - }); - }} - placeholder={t('form.fields.title.placeholder')} - autoFocus - contentEditable - /> + + + {objectType === 'question' && ( + + {t('form.fields.title.label')} + { + handleInput({ + title: { + value: e.target.value, + isInvalid: false, + errorMsg: '', + }, + }); + }} + placeholder={t('form.fields.title.placeholder')} + autoFocus + contentEditable + /> - - {formData.title.errorMsg} - - + + {formData.title.errorMsg} + + + )} - - {t('form.fields.body.label')} - - - {t('form.fields.tags.label')} - -
-
- - - - + {objectType !== 'comment' && ( + + + {objectType === 'question' + ? t('form.fields.body.label') + : t('form.fields.answer.label')} + + + )} + + {objectType === 'question' && ( + + {t('form.fields.tags.label')} + + )} + + {objectType === 'comment' && ( +
+
+ Comment + +