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
- />
+
+ )}
-
- {t('form.fields.body.label')}
-
- {
- handleInput({
- content: { value, errorMsg: '', isInvalid: false },
- });
- }}
- className={classNames(
- 'form-control p-0',
- focusEditor ? 'focus' : '',
- )}
- onFocus={() => {
- setFocusEditor(true);
- }}
- onBlur={() => {
- setFocusEditor(false);
- }}
- />
-
- {formData.content.errorMsg}
-
-
-
- {t('form.fields.tags.label')}
-
- {
- handleInput({
- tags: { value, errorMsg: '', isInvalid: false },
- });
- }}
- showRequiredTag
- maxTagLength={5}
- />
-
- {formData.tags.errorMsg}
-
-
-
-
-
-
- {t('close', { keyPrefix: 'btns' })}
-
-
- {t('submit', { keyPrefix: 'btns' })}
-
-
+ {objectType !== 'comment' && (
+
+
+ {objectType === 'question'
+ ? t('form.fields.body.label')
+ : t('form.fields.answer.label')}
+
+
+ {
+ handleInput({
+ content: { value, errorMsg: '', isInvalid: false },
+ });
+ }}
+ className={classNames(
+ 'form-control p-0',
+ focusEditor ? 'focus' : '',
+ )}
+ onFocus={() => {
+ setFocusEditor(true);
+ }}
+ onBlur={() => {
+ setFocusEditor(false);
+ }}
+ />
+
+ {formData.content.errorMsg}
+
+
+ )}
+
+ {objectType === 'question' && (
+
+ {t('form.fields.tags.label')}
+
+ {
+ handleInput({
+ tags: { value, errorMsg: '', isInvalid: false },
+ });
+ }}
+ showRequiredTag
+ maxTagLength={5}
+ />
+
+ {formData.tags.errorMsg}
+
+
+ )}
+
+ {objectType === 'comment' && (
+
+
+
Comment
+
+
+
+
+ {formData.content.errorMsg}
+
+
+ )}
+
+
+ onClose(false)}>
+ {t('close', { keyPrefix: 'btns' })}
+
+
+ {t('submit', { keyPrefix: 'btns' })}
+
+
+
);
};
diff --git a/ui/src/pages/Review/components/Filter/index.tsx b/ui/src/pages/Review/components/Filter/index.tsx
index 2eccf86a..16a392e2 100644
--- a/ui/src/pages/Review/components/Filter/index.tsx
+++ b/ui/src/pages/Review/components/Filter/index.tsx
@@ -1,29 +1,36 @@
-import { useState } from 'react';
+import { FC } from 'react';
import { Card, Form } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
-const Index = () => {
+import * as Type from '@/common/interface';
+
+interface IProps {
+ list: Type.ReviewTypeItem[] | undefined;
+ checked: string;
+ callback: (type: string) => void;
+}
+
+const Index: FC = ({ list, checked, callback }) => {
const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
- const [checked, setValue] = useState(false);
return (
{t('filter', { keyPrefix: 'btns' })}
{t('filter_label')}
- setValue(e.target.checked)}
- />
-
- setValue(e.target.checked)}
- />
+ {list?.map((item) => {
+ return (
+ callback(item.name)}
+ />
+ );
+ })}
diff --git a/ui/src/pages/Review/components/FlagContent/index.tsx b/ui/src/pages/Review/components/FlagContent/index.tsx
index fdead753..526e273b 100644
--- a/ui/src/pages/Review/components/FlagContent/index.tsx
+++ b/ui/src/pages/Review/components/FlagContent/index.tsx
@@ -1,101 +1,235 @@
-import { FC } from 'react';
-import { Card, Badge } from 'react-bootstrap';
+import { FC, useEffect, useState } from 'react';
+import { Card, Alert, Stack, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
-import { BaseUserCard, Tag, FormatTime, Avatar } from '@/components';
+import classNames from 'classnames';
-const tag = [
- {
- display_name: 'bug',
- slug_name: 'bug',
- original_text: '111',
- recommend: true,
- },
- {
- display_name: 'react',
- slug_name: 'react',
- original_text: '222',
- reserved: true,
- },
- {
- display_name: 'test',
- slug_name: 'test',
- original_text: '111',
- recommend: false,
- reserved: false,
- },
-];
+import { getFlagReviewPostList, putFlagReviewAction } from '@/services';
+import { BaseUserCard, Tag, FormatTime } from '@/components';
+import { pathFactory } from '@/router/pathFactory';
+import { scrollToDocTop } from '@/utils';
+import type * as Type from '@/common/interface';
+import { ADMIN_LIST_STATUS } from '@/common/constants';
+import ApproveDropdown from '../ApproveDropdown';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
- const objectType = 'question';
+ const [noTasks, setNoTasks] = useState(false);
+ const [isLoading, setIsLoading] = useState(false);
+ const [page, setPage] = useState(1);
+ const [reviewResp, setReviewResp] = useState();
+ const flagItemData = reviewResp?.list[0] as Type.FlagReviewItem;
+
+ console.log('reviewResp', reviewResp);
+
+ const resolveNextOne = (resp, pageNumber) => {
+ const { count, list = [] } = resp;
+ // auto rollback
+ if (!list.length && count && page !== 1) {
+ pageNumber = 1;
+ setPage(pageNumber);
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ queryNextOne(pageNumber);
+ return;
+ }
+ if (pageNumber !== page) {
+ setPage(pageNumber);
+ }
+ setReviewResp(resp);
+ if (!list.length) {
+ setNoTasks(true);
+ }
+ setTimeout(() => {
+ scrollToDocTop();
+ }, 150);
+ };
+
+ const queryNextOne = (pageNumber) => {
+ getFlagReviewPostList(pageNumber)
+ .then((resp) => {
+ resolveNextOne(resp, pageNumber);
+ })
+ .catch((ex) => {
+ console.error('review next error: ', ex);
+ });
+ };
+
+ useEffect(() => {
+ queryNextOne(page);
+ }, []);
+
+ const handlingApprove = () => {
+ if (!flagItemData) {
+ return;
+ }
+ queryNextOne(page);
+ };
+
+ const handleIgnore = () => {
+ setIsLoading(true);
+ putFlagReviewAction({
+ operation_type: 'ignore_report',
+ flag_id: String(flagItemData?.flag_id),
+ })
+ .then(() => {
+ queryNextOne(page + 1);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ };
+
+ const {
+ object_type,
+ submitter_user,
+ author_user_info,
+ object_status,
+ reason,
+ } = flagItemData || {
+ object_type: '',
+ submitter_user: null,
+ author_user_info: null,
+ reason: null,
+ object_status: 0,
+ };
+ let itemLink = '';
+ let itemId = '';
+ let itemTimePrefix = '';
+
+ if (object_type === 'question') {
+ itemLink = pathFactory.questionLanding(
+ String(flagItemData?.question_id),
+ flagItemData?.title,
+ );
+ itemId = String(flagItemData?.question_id);
+ itemTimePrefix = 'asked';
+ } else if (object_type === 'answer') {
+ itemLink = pathFactory.answerLanding({
+ // @ts-ignore
+ questionId: flagItemData?.question_id,
+ slugTitle: flagItemData?.title,
+ answerId: String(flagItemData?.object_id),
+ });
+ itemId = String(flagItemData?.object_id);
+ itemTimePrefix = 'answered';
+ } else if (object_type === 'comment') {
+ if (flagItemData?.question_id && flagItemData?.answer_id) {
+ itemLink = `${pathFactory.answerLanding({
+ questionId: flagItemData?.question_id,
+ answerId: flagItemData?.answer_id,
+ })}?commentId=${flagItemData?.comment_id}`;
+ } else {
+ itemLink = `${pathFactory.questionLanding(
+ String(flagItemData?.question_id),
+ flagItemData?.title,
+ )}?commentId=${flagItemData?.comment_id}`;
+ }
+ itemId = String(flagItemData?.comment_id);
+ itemTimePrefix = 'commented';
+ }
+
+ if (noTasks) return null;
return (
- {t('flag_type', { type: 'post' })}
+
+ {object_type !== 'user' ? t('flag_post') : t('flag_user')}
+
+
+
+
+ {flagItemData?.submit_at && (
+
+ )}
+
+
+
+ {object_type !== 'user'
+ ? t('flag_post_type', { type: reason?.name })
+ : t('flag_user_type', { type: reason?.name })}
+
+
+
-
- How do I test weather variable against multiple
-
- {objectType === 'question' && (
-
- {tag?.map((item) => {
- return (
-
- );
- })}
-
+
+ {t(object_type, { keyPrefix: 'btns' })}
+
+ #{itemId}
+
+
+ {object_type === 'question' && (
+ <>
+
{flagItemData?.title}
+
+ {flagItemData?.tags?.map((item) => {
+ return (
+
+ );
+ })}
+
+ >
)}
- Python is a multi-paradigm, dynamically typed, multi-purpose
- programming language. It is designed to be quick to learn,
- understand, and use, and enforces a clean and uniform syntax. Please
- note that Python 2 is officially out of support as of 2020-01-01.
- For version-specific Python questions, add the [python-2.7] or
- [python-3.x] tag. When using a Python variant library (e.g. Pandas,
- NumPy), please include it in the tags.
+ {flagItemData?.original_text}
-
-
normal
+
+
+
+ {t(ADMIN_LIST_STATUS[object_status]?.name, {
+ keyPrefix: 'admin.questions',
+ })}
+
+ {flagItemData?.object_show_status === 2 && (
+
+ {t(ADMIN_LIST_STATUS.unlist.name, { keyPrefix: 'btns' })}
+
+ )}
+
-
+
-
-
-
-
-
- 111
@111
-
-
- I'm a web developer with in-depth experience in UI/UX design.
-
-
280 {t('reputation')}
-
-
+
+
+ {t('approve_this_type', { type: 'revision' })}
+
+
+
+ {t('ignore', { keyPrefix: 'btns' })}
+
+
+
);
};
diff --git a/ui/src/pages/Review/components/SuggestEditContent/index.tsx b/ui/src/pages/Review/components/SuggestEditContent/index.tsx
new file mode 100644
index 00000000..9e45b97d
--- /dev/null
+++ b/ui/src/pages/Review/components/SuggestEditContent/index.tsx
@@ -0,0 +1,246 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+
+import { FC, useEffect, useState } from 'react';
+import { Alert, Stack, Button, Card } from 'react-bootstrap';
+import { Link } from 'react-router-dom';
+import { useTranslation } from 'react-i18next';
+
+import { BaseUserCard, FormatTime, DiffContent } from '@/components';
+import { getSuggestReviewList, revisionAudit } from '@/services';
+import { pathFactory } from '@/router/pathFactory';
+import { scrollToDocTop } from '@/utils';
+import type * as Type from '@/common/interface';
+
+const Index: FC = () => {
+ const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
+ const [isLoading, setIsLoading] = useState(false);
+ const [noTasks, setNoTasks] = useState(false);
+ const [page, setPage] = useState(1);
+ const [reviewResp, setReviewResp] = useState
();
+ const ro = reviewResp?.list[0];
+ const { info, type, unreviewed_info } = ro || {
+ info: null,
+ type: '',
+ unreviewed_info: null,
+ };
+ const resolveNextOne = (resp, pageNumber) => {
+ const { count, list = [] } = resp;
+ // auto rollback
+ if (!list.length && count && page !== 1) {
+ pageNumber = 1;
+ setPage(pageNumber);
+ // eslint-disable-next-line @typescript-eslint/no-use-before-define
+ queryNextOne(pageNumber);
+ return;
+ }
+ if (pageNumber !== page) {
+ setPage(pageNumber);
+ }
+ setReviewResp(resp);
+ if (!list.length) {
+ setNoTasks(true);
+ }
+ setTimeout(() => {
+ scrollToDocTop();
+ }, 150);
+ };
+ const queryNextOne = (pageNumber) => {
+ getSuggestReviewList(pageNumber)
+ .then((resp) => {
+ resolveNextOne(resp, pageNumber);
+ })
+ .catch((ex) => {
+ console.error('review next error: ', ex);
+ });
+ };
+ const reviewInfo = unreviewed_info?.content;
+
+ const handlingApprove = () => {
+ if (!unreviewed_info) {
+ return;
+ }
+ setIsLoading(true);
+ revisionAudit(unreviewed_info.id, 'approve')
+ .then(() => {
+ queryNextOne(page);
+ })
+ .catch((ex) => {
+ console.error('revisionAudit approve error: ', ex);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ };
+
+ const handlingReject = () => {
+ if (!unreviewed_info) {
+ return;
+ }
+ setIsLoading(true);
+ revisionAudit(unreviewed_info.id, 'reject')
+ .then(() => {
+ queryNextOne(page);
+ })
+ .catch((ex) => {
+ console.error('revisionAudit reject error: ', ex);
+ })
+ .finally(() => {
+ setIsLoading(false);
+ });
+ };
+
+ let itemLink = '';
+ let itemId = '';
+ let editSummary = unreviewed_info?.reason;
+ const editor = unreviewed_info?.user_info;
+ const editTime = unreviewed_info?.create_at;
+ if (type === 'question') {
+ itemLink = pathFactory.questionLanding(info?.object_id, info?.url_title);
+ itemId = info?.object_id;
+ editSummary ||= t('edit_question');
+ } else if (type === 'answer') {
+ itemLink = pathFactory.answerLanding({
+ // @ts-ignore
+ questionId: unreviewed_info.content.question_id,
+ slugTitle: info?.url_title,
+ answerId: unreviewed_info.object_id,
+ });
+ itemId = unreviewed_info.object_id;
+ editSummary ||= t('edit_answer');
+ } else if (type === 'tag') {
+ const tagInfo = unreviewed_info.content as Type.Tag;
+ itemLink = pathFactory.tagLanding(tagInfo.slug_name);
+ itemId = tagInfo?.tag_id || tagInfo.slug_name;
+ editSummary ||= t('edit_tag');
+ }
+ useEffect(() => {
+ queryNextOne(page);
+ }, []);
+
+ if (noTasks) return null;
+
+ let newData: Record = {};
+ let oldData: Record = {};
+ let diffOpts: Partial<{
+ showTitle: boolean;
+ showTagUrlSlug: boolean;
+ }> = {
+ showTitle: true,
+ showTagUrlSlug: true,
+ };
+ if (type === 'question' && info && reviewInfo && 'content' in reviewInfo) {
+ newData = {
+ title: reviewInfo.title,
+ original_text: reviewInfo.content,
+ tags: reviewInfo.tags,
+ };
+ oldData = {
+ title: info.title,
+ original_text: info.content,
+ tags: info.tags,
+ };
+ }
+ if (type === 'answer' && info && reviewInfo && 'content' in reviewInfo) {
+ newData = {
+ original_text: reviewInfo.content,
+ };
+ oldData = {
+ original_text: info.content,
+ };
+ }
+
+ if (type === 'tag' && info && reviewInfo) {
+ newData = {
+ original_text: reviewInfo.original_text,
+ };
+ oldData = {
+ original_text: info.content,
+ };
+ diffOpts = { showTitle: false, showTagUrlSlug: false };
+ }
+
+ return (
+
+
+ {t('suggest_type_edit', {
+ type:
+ type === 'question' || type === 'answer'
+ ? t('post_lowercase', { keyPrefix: 'btns' })
+ : type,
+ })}
+
+
+
+
+
+ {editTime && (
+
+ )}
+
+
+ {editSummary}
+
+
+
+
+ {t(type, { keyPrefix: 'btns' })}
+
+ #{itemId}
+
+
+
+
+
+
+
+ {t('approve_this_type', { type: 'revision' })}
+
+
+ {t('approve', { keyPrefix: 'btns' })}
+
+
+ {t('reject', { keyPrefix: 'btns' })}
+
+
+
+
+ );
+};
+
+export default Index;
diff --git a/ui/src/pages/Review/components/index.ts b/ui/src/pages/Review/components/index.ts
index 74f4f1df..ff1950b1 100644
--- a/ui/src/pages/Review/components/index.ts
+++ b/ui/src/pages/Review/components/index.ts
@@ -1,6 +1,13 @@
import Filter from './Filter';
import ApproveDropdown from './ApproveDropdown';
import EditPostModal from './EditPostModal';
+import SuggestEditContent from './SuggestEditContent';
import FlagContent from './FlagContent';
-export { Filter, ApproveDropdown, EditPostModal, FlagContent };
+export {
+ Filter,
+ ApproveDropdown,
+ EditPostModal,
+ FlagContent,
+ SuggestEditContent,
+};
diff --git a/ui/src/pages/Review/index.tsx b/ui/src/pages/Review/index.tsx
index 77af95b9..c73ba9b7 100644
--- a/ui/src/pages/Review/index.tsx
+++ b/ui/src/pages/Review/index.tsx
@@ -18,260 +18,62 @@
*/
import { FC, useEffect, useState } from 'react';
-import { Row, Col, Alert, Stack, Button, Card } from 'react-bootstrap';
-import { Link } from 'react-router-dom';
+import { Row, Col } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { usePageTags } from '@/hooks';
-import { BaseUserCard, FormatTime, Empty, DiffContent } from '@/components';
-import { getReviewList, revisionAudit } from '@/services';
-import { pathFactory } from '@/router/pathFactory';
-import { scrollToDocTop } from '@/utils';
+import { Empty } from '@/components';
+import { getReviewType } from '@/services';
import type * as Type from '@/common/interface';
-import { Filter, ApproveDropdown, FlagContent } from './components';
+import { Filter, FlagContent, SuggestEditContent } from './components';
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
- const [isLoading, setIsLoading] = useState(false);
- const [noTasks, setNoTasks] = useState(false);
- const [page, setPage] = useState(1);
- const [reviewResp, setReviewResp] = useState();
- const ro = reviewResp?.list[0];
- const { info, type, unreviewed_info } = ro || {
- info: null,
- type: '',
- unreviewed_info: null,
- };
- const resolveNextOne = (resp, pageNumber) => {
- const { count, list = [] } = resp;
- // auto rollback
- if (!list.length && count && page !== 1) {
- pageNumber = 1;
- setPage(pageNumber);
- // eslint-disable-next-line @typescript-eslint/no-use-before-define
- queryNextOne(pageNumber);
- return;
- }
- if (pageNumber !== page) {
- setPage(pageNumber);
- }
- setReviewResp(resp);
- if (!list.length) {
- setNoTasks(true);
- }
- setTimeout(() => {
- scrollToDocTop();
- }, 150);
- };
- const queryNextOne = (pageNumber) => {
- getReviewList(pageNumber)
+ const [reviewTypeList, setReviewTypeList] = useState();
+ const [currentReviewType, setCurrentReviewType] = useState('');
+
+ const fetchReviewType = () => {
+ getReviewType()
.then((resp) => {
- resolveNextOne(resp, pageNumber);
+ const filterData = resp.filter((item) => item.todo_amount > 0);
+ if (filterData.length > 0) {
+ setCurrentReviewType(filterData[0].name);
+ }
+ setReviewTypeList(resp);
})
.catch((ex) => {
- console.error('review next error: ', ex);
+ console.error('getReviewType error: ', ex);
});
};
- const reviewInfo = unreviewed_info?.content;
- const handlingSkip = () => {
- queryNextOne(page + 1);
- };
- const handlingApprove = () => {
- if (!unreviewed_info) {
- return;
- }
- setIsLoading(true);
- revisionAudit(unreviewed_info.id, 'approve')
- .then(() => {
- queryNextOne(page);
- })
- .catch((ex) => {
- console.error('revisionAudit approve error: ', ex);
- })
- .finally(() => {
- setIsLoading(false);
- });
- };
- const handlingReject = () => {
- if (!unreviewed_info) {
- return;
- }
- setIsLoading(true);
- revisionAudit(unreviewed_info.id, 'reject')
- .then(() => {
- queryNextOne(page);
- })
- .catch((ex) => {
- console.error('revisionAudit reject error: ', ex);
- })
- .finally(() => {
- setIsLoading(false);
- });
- };
- let itemLink = '';
- let itemId = '';
- let editSummary = unreviewed_info?.reason;
- const editor = unreviewed_info?.user_info;
- const editTime = unreviewed_info?.create_at;
- if (type === 'question') {
- itemLink = pathFactory.questionLanding(info?.object_id, info?.url_title);
- itemId = info?.object_id;
- editSummary ||= t('edit_question');
- } else if (type === 'answer') {
- itemLink = pathFactory.answerLanding({
- // @ts-ignore
- questionId: unreviewed_info.content.question_id,
- slugTitle: info?.url_title,
- answerId: unreviewed_info.object_id,
- });
- itemId = unreviewed_info.object_id;
- editSummary ||= t('edit_answer');
- } else if (type === 'tag') {
- const tagInfo = unreviewed_info.content as Type.Tag;
- itemLink = pathFactory.tagLanding(tagInfo.slug_name);
- itemId = tagInfo?.tag_id || tagInfo.slug_name;
- editSummary ||= t('edit_tag');
- }
+
useEffect(() => {
- queryNextOne(page);
+ fetchReviewType();
}, []);
+
usePageTags({
title: t('review'),
});
- let newData: Record = {};
- let oldData: Record = {};
- let diffOpts: Partial<{
- showTitle: boolean;
- showTagUrlSlug: boolean;
- }> = {
- showTitle: true,
- showTagUrlSlug: true,
- };
- if (type === 'question' && info && reviewInfo && 'content' in reviewInfo) {
- newData = {
- title: reviewInfo.title,
- original_text: reviewInfo.content,
- tags: reviewInfo.tags,
- };
- oldData = {
- title: info.title,
- original_text: info.content,
- tags: info.tags,
- };
- }
- if (type === 'answer' && info && reviewInfo && 'content' in reviewInfo) {
- newData = {
- original_text: reviewInfo.content,
- };
- oldData = {
- original_text: info.content,
- };
- }
-
- if (type === 'tag' && info && reviewInfo) {
- newData = {
- original_text: reviewInfo.original_text,
- };
- oldData = {
- original_text: info.content,
- };
- diffOpts = { showTitle: false, showTagUrlSlug: false };
- }
-
return (
{t('review')}
- {!noTasks && ro && (
-
-
- {t('suggest_type_edit', {
- type:
- type === 'question' || type === 'answer'
- ? t('post_lowercase', { keyPrefix: 'btns' })
- : type,
- })}
-
-
-
-
-
- {editTime && (
-
- )}
-
-
- {editSummary}
-
-
-
-
- {t(type, { keyPrefix: 'btns' })}
-
- #{itemId}
-
-
-
-
-
-
-
- {t('approve_this_type', { type: 'revision' })}
-
-
- {t('approve', { keyPrefix: 'btns' })}
-
-
-
- {t('reject', { keyPrefix: 'btns' })}
-
-
- {t('ignore', { keyPrefix: 'btns' })}
-
-
- {t('skip', { keyPrefix: 'btns' })}
-
-
-
-
- )}
- {noTasks && {t('empty')} }
+ {currentReviewType === 'suggested_post_edit' && }
+ {/* {currentReviewType === 'flagged_post' && } */}
+ {t('empty')}
-
+ {
+ setCurrentReviewType(name);
+ }}
+ />
);
diff --git a/ui/src/router/pathFactory.ts b/ui/src/router/pathFactory.ts
index ec326acf..ea353604 100644
--- a/ui/src/router/pathFactory.ts
+++ b/ui/src/router/pathFactory.ts
@@ -41,7 +41,7 @@ const questionLanding = (questionId: string, slugTitle: string = '') => {
}
// @ts-ignore
if (/[13]/.test(seo.permalink) && slugTitle) {
- return `/questions/${questionId}/${slugTitle}`;
+ return `/questions/${questionId}/${encodeURIComponent(slugTitle)}`;
}
return `/questions/${questionId}`;
diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts
index 89d38a95..0d718423 100644
--- a/ui/src/services/client/index.ts
+++ b/ui/src/services/client/index.ts
@@ -29,3 +29,4 @@ export * from './timeline';
export * from './revision';
export * from './user';
export * from './Oauth';
+export * from './review';
diff --git a/ui/src/services/client/review.ts b/ui/src/services/client/review.ts
new file mode 100644
index 00000000..09278914
--- /dev/null
+++ b/ui/src/services/client/review.ts
@@ -0,0 +1,40 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+// import useSWR from 'swr';
+
+import request from '@/utils/request';
+import * as Type from '@/common/interface';
+
+export const getSuggestReviewList = (page: number) => {
+ const apiUrl = `/answer/api/v1/revisions/unreviewed?page=${page}`;
+ return request.get(apiUrl);
+};
+
+export const getReviewType = () => {
+ return request.get('/answer/api/v1/reviewing/type');
+};
+
+export const getFlagReviewPostList = (page: number) => {
+ const apiUrl = `/answer/api/v1/report/unreviewed/post?page=${page}`;
+ return request.get(apiUrl);
+};
+
+export const putFlagReviewAction = (params: Type.PutFlagReviewParams) => {
+ return request.put('/answer/api/v1/report/review', params);
+};
diff --git a/ui/src/services/client/revision.ts b/ui/src/services/client/revision.ts
index b71d7f24..489730e4 100644
--- a/ui/src/services/client/revision.ts
+++ b/ui/src/services/client/revision.ts
@@ -18,7 +18,6 @@
*/
import request from '@/utils/request';
-import * as Type from '@/common/interface';
export const editCheck = (id: string, passingError: boolean = false) => {
const apiUrl = `/answer/api/v1/revisions/edit/check?id=${id}`;
@@ -34,8 +33,3 @@ export const revisionAudit = (id: string, operation: 'approve' | 'reject') => {
operation,
});
};
-
-export const getReviewList = (page: number) => {
- const apiUrl = `/answer/api/v1/revisions/unreviewed?page=${page}`;
- return request.get(apiUrl);
-};