feat: add flag review content
This commit is contained in:
+11
-10
@@ -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
|
||||
|
||||
+2
-2
@@ -1628,8 +1628,8 @@ ui:
|
||||
page_title: 编辑
|
||||
restrict_answer:
|
||||
title: 限制一个回答
|
||||
label: 每个用户对于每个问题只能有一个回答
|
||||
text: "用户可以使用编辑按钮优化已有的回答"
|
||||
label: 每个用户只能为同一问题写一个回答
|
||||
text: "关闭以允许用户对同一问题编写多个回答,这可能会导致回答不集中。"
|
||||
recommend_tags:
|
||||
label: 推荐标签
|
||||
text: "请在上方输入标签固定链接,每行一个标签。"
|
||||
|
||||
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -118,13 +118,13 @@ const Answers: FC = () => {
|
||||
className="text-break text-wrap"
|
||||
rel="noreferrer">
|
||||
{li.question_info.title}
|
||||
{li.accepted === 2 && (
|
||||
<Icon
|
||||
name="check-circle-fill"
|
||||
className="ms-2 text-success"
|
||||
/>
|
||||
)}
|
||||
</a>
|
||||
{li.accepted === 2 && (
|
||||
<Icon
|
||||
name="check-circle-fill"
|
||||
className="ms-2 text-success"
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<div
|
||||
className="text-truncate-2 small"
|
||||
|
||||
@@ -153,9 +153,9 @@ const Questions: FC = () => {
|
||||
<span
|
||||
className={classNames(
|
||||
'badge',
|
||||
ADMIN_LIST_STATUS.unlisted.variant,
|
||||
ADMIN_LIST_STATUS.unlist.variant,
|
||||
)}>
|
||||
{t(ADMIN_LIST_STATUS.unlisted.name)}
|
||||
{t(ADMIN_LIST_STATUS.unlist.name)}
|
||||
</span>
|
||||
)}
|
||||
</td>
|
||||
|
||||
@@ -46,7 +46,7 @@ const SearchQuestion = ({ similarQuestions }) => {
|
||||
<ListGroup.Item
|
||||
action
|
||||
as="a"
|
||||
className="link-dark text-wrap text-break"
|
||||
className="link-dark text-wrap text-break grid gap-0 row-gap-3"
|
||||
key={item.id}
|
||||
href={pathFactory.questionLanding(item.id, item.url_title)}
|
||||
target="_blank">
|
||||
@@ -56,7 +56,7 @@ const SearchQuestion = ({ similarQuestions }) => {
|
||||
: null}
|
||||
|
||||
{item.accepted_answer ? (
|
||||
<span className="small ms-3 text-success">
|
||||
<span className="small ms-3 text-success d-inline-block">
|
||||
<Icon type="bi" name="check-circle-fill" />
|
||||
<span className="ms-1">
|
||||
{t('x_answers', {
|
||||
@@ -67,7 +67,7 @@ const SearchQuestion = ({ similarQuestions }) => {
|
||||
</span>
|
||||
) : (
|
||||
item.answer_count > 0 && (
|
||||
<span className="small ms-3 text-secondary">
|
||||
<span className="small ms-3 text-secondary d-inline-block">
|
||||
<Icon type="bi" name="chat-square-text-fill" />
|
||||
<span className="ms-1">
|
||||
{t('x_answers', {
|
||||
|
||||
@@ -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<IProps> = ({
|
||||
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 (
|
||||
<div>
|
||||
<Dropdown>
|
||||
<Dropdown.Toggle
|
||||
as={Button}
|
||||
disabled={isLoading}
|
||||
variant="outline-primary"
|
||||
id="dropdown-basic">
|
||||
Approve
|
||||
{t('approve', { keyPrefix: 'btns' })}
|
||||
</Dropdown.Toggle>
|
||||
|
||||
<Dropdown.Menu>
|
||||
<Dropdown.Item href="#/action-1">Deactivate user</Dropdown.Item>
|
||||
<Dropdown.Item href="#/action-2">Suspend user</Dropdown.Item>
|
||||
<Dropdown.Item href="#/action-3">Delete user</Dropdown.Item>
|
||||
<Dropdown.Item onClick={() => handleActionEdit()}>
|
||||
{t('edit_post')}
|
||||
</Dropdown.Item>
|
||||
{curFilter === 'normal' && objectType === 'question' && (
|
||||
<Dropdown.Item onClick={() => handleAction('close')}>
|
||||
{t('close', { keyPrefix: 'btns' })}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{curFilter !== 'deleted' && (
|
||||
<Dropdown.Item onClick={() => handleAction('delete')}>
|
||||
{t('delete', { keyPrefix: 'btns' })}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
{objectType === 'question' && (
|
||||
<>
|
||||
<Dropdown.Divider />
|
||||
{itemData?.object_show_status !== 2 && (
|
||||
<Dropdown.Item onClick={() => handleAction('unlist')}>
|
||||
{t('unlist_post')}
|
||||
</Dropdown.Item>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Dropdown.Menu>
|
||||
</Dropdown>
|
||||
<EditPostModal
|
||||
visible={showEditPostModal}
|
||||
handleClose={handleEditPostModalState}
|
||||
objectType={objectType}
|
||||
originalData={{
|
||||
flag_id: itemData?.flag_id || '',
|
||||
id: itemData?.object_id || '',
|
||||
title: itemData?.title || '',
|
||||
content: itemData?.original_text || '',
|
||||
tags: itemData?.tags || [],
|
||||
question_id: itemData?.question_id || '',
|
||||
answer_id: itemData?.answer_id || '',
|
||||
}}
|
||||
callback={approveCallback}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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<string>;
|
||||
}
|
||||
|
||||
const Index: FC<Props> = ({ 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<Props> = ({
|
||||
originalData,
|
||||
visible = false,
|
||||
objectType,
|
||||
handleClose,
|
||||
callback,
|
||||
}) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'ask' });
|
||||
const [formData, setFormData] = useState<FormDataItem>(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<FormDataItem>) => {
|
||||
if (!loaded) {
|
||||
return;
|
||||
}
|
||||
setFormData({
|
||||
...formData,
|
||||
...data,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
||||
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<HTMLFormElement>) => {
|
||||
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<Props> = ({ 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 (
|
||||
<Modal
|
||||
show={visible}
|
||||
onHide={handleClose}
|
||||
onHide={() => onClose(false)}
|
||||
className="w-100"
|
||||
dialogClassName="edit-post-modal">
|
||||
<Modal.Header closeButton>
|
||||
<Modal.Title>Edit post</Modal.Title>
|
||||
<Modal.Title>
|
||||
{t('edit_post', { keyPrefix: 'page_review' })}
|
||||
</Modal.Title>
|
||||
</Modal.Header>
|
||||
<Modal.Body>
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Form.Group controlId="title" className="mb-3">
|
||||
<Form.Label>{t('form.fields.title.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.title.value}
|
||||
isInvalid={formData.title.isInvalid}
|
||||
onChange={(e) => {
|
||||
handleInput({
|
||||
title: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder={t('form.fields.title.placeholder')}
|
||||
autoFocus
|
||||
contentEditable
|
||||
/>
|
||||
<Form noValidate onSubmit={handleSubmit}>
|
||||
<Modal.Body>
|
||||
{objectType === 'question' && (
|
||||
<Form.Group controlId="title" className="mb-3">
|
||||
<Form.Label>{t('form.fields.title.label')}</Form.Label>
|
||||
<Form.Control
|
||||
type="text"
|
||||
value={formData.title.value}
|
||||
isInvalid={formData.title.isInvalid}
|
||||
onChange={(e) => {
|
||||
handleInput({
|
||||
title: {
|
||||
value: e.target.value,
|
||||
isInvalid: false,
|
||||
errorMsg: '',
|
||||
},
|
||||
});
|
||||
}}
|
||||
placeholder={t('form.fields.title.placeholder')}
|
||||
autoFocus
|
||||
contentEditable
|
||||
/>
|
||||
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.title.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.title.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
)}
|
||||
|
||||
<Form.Group controlId="body">
|
||||
<Form.Label>{t('form.fields.body.label')}</Form.Label>
|
||||
<Form.Control
|
||||
defaultValue={formData.content.value}
|
||||
isInvalid={formData.content.isInvalid}
|
||||
hidden
|
||||
/>
|
||||
<Editor
|
||||
value={formData.content.value}
|
||||
onChange={(value) => {
|
||||
handleInput({
|
||||
content: { value, errorMsg: '', isInvalid: false },
|
||||
});
|
||||
}}
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
focusEditor ? 'focus' : '',
|
||||
)}
|
||||
onFocus={() => {
|
||||
setFocusEditor(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusEditor(false);
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.content.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="tags" className="my-3">
|
||||
<Form.Label>{t('form.fields.tags.label')}</Form.Label>
|
||||
<Form.Control
|
||||
defaultValue={JSON.stringify(formData.tags.value)}
|
||||
isInvalid={formData.tags.isInvalid}
|
||||
hidden
|
||||
/>
|
||||
<TagSelector
|
||||
value={formData.tags.value}
|
||||
onChange={(value) => {
|
||||
handleInput({
|
||||
tags: { value, errorMsg: '', isInvalid: false },
|
||||
});
|
||||
}}
|
||||
showRequiredTag
|
||||
maxTagLength={5}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.tags.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={handleClose}>
|
||||
{t('close', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<Button variant="primary" onClick={handleClose}>
|
||||
{t('submit', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
{objectType !== 'comment' && (
|
||||
<Form.Group controlId="body">
|
||||
<Form.Label>
|
||||
{objectType === 'question'
|
||||
? t('form.fields.body.label')
|
||||
: t('form.fields.answer.label')}
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
defaultValue={formData.content.value}
|
||||
isInvalid={formData.content.isInvalid}
|
||||
hidden
|
||||
/>
|
||||
<Editor
|
||||
value={formData.content.value}
|
||||
onChange={(value) => {
|
||||
handleInput({
|
||||
content: { value, errorMsg: '', isInvalid: false },
|
||||
});
|
||||
}}
|
||||
className={classNames(
|
||||
'form-control p-0',
|
||||
focusEditor ? 'focus' : '',
|
||||
)}
|
||||
onFocus={() => {
|
||||
setFocusEditor(true);
|
||||
}}
|
||||
onBlur={() => {
|
||||
setFocusEditor(false);
|
||||
}}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.content.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
)}
|
||||
|
||||
{objectType === 'question' && (
|
||||
<Form.Group controlId="tags" className="my-3">
|
||||
<Form.Label>{t('form.fields.tags.label')}</Form.Label>
|
||||
<Form.Control
|
||||
defaultValue={JSON.stringify(formData.tags.value)}
|
||||
isInvalid={formData.tags.isInvalid}
|
||||
hidden
|
||||
/>
|
||||
<TagSelector
|
||||
value={formData.tags.value}
|
||||
onChange={(value) => {
|
||||
handleInput({
|
||||
tags: { value, errorMsg: '', isInvalid: false },
|
||||
});
|
||||
}}
|
||||
showRequiredTag
|
||||
maxTagLength={5}
|
||||
/>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.tags.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
)}
|
||||
|
||||
{objectType === 'comment' && (
|
||||
<div className="w-100">
|
||||
<div
|
||||
className={classNames('custom-form-control', {
|
||||
'is-invalid': formData.content.isInvalid,
|
||||
})}>
|
||||
<Form.Label>Comment</Form.Label>
|
||||
<Mentions
|
||||
pageUsers={pageUsers.getUsers()}
|
||||
onSelected={handleSelected}>
|
||||
<TextArea
|
||||
size="sm"
|
||||
rows={4}
|
||||
value={parseEditMentionUser(formData.content.value)}
|
||||
onChange={(e) => {
|
||||
handleInput({
|
||||
content: {
|
||||
value: e.target.value,
|
||||
errorMsg: '',
|
||||
isInvalid: false,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Mentions>
|
||||
</div>
|
||||
<Form.Control.Feedback type="invalid">
|
||||
{formData.content.errorMsg}
|
||||
</Form.Control.Feedback>
|
||||
</div>
|
||||
)}
|
||||
</Modal.Body>
|
||||
<Modal.Footer>
|
||||
<Button variant="secondary" onClick={() => onClose(false)}>
|
||||
{t('close', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<Button variant="primary" type="submit">
|
||||
{t('submit', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</Modal.Footer>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<IProps> = ({ list, checked, callback }) => {
|
||||
const { t } = useTranslation('translation', { keyPrefix: 'page_review' });
|
||||
const [checked, setValue] = useState(false);
|
||||
return (
|
||||
<Card>
|
||||
<Card.Header>{t('filter', { keyPrefix: 'btns' })}</Card.Header>
|
||||
<Card.Body>
|
||||
<Form.Group>
|
||||
<Form.Label>{t('filter_label')}</Form.Label>
|
||||
<Form.Check
|
||||
type="radio"
|
||||
label="Queued post (99+)"
|
||||
checked={checked}
|
||||
onChange={(e) => setValue(e.target.checked)}
|
||||
/>
|
||||
|
||||
<Form.Check
|
||||
type="radio"
|
||||
label="Queued post (199+)"
|
||||
checked={checked}
|
||||
onChange={(e) => setValue(e.target.checked)}
|
||||
/>
|
||||
{list?.map((item) => {
|
||||
return (
|
||||
<Form.Check
|
||||
key={item.name}
|
||||
type="radio"
|
||||
id={item.name}
|
||||
disabled={item.todo_amount <= 0}
|
||||
label={`${item.label} (${item.todo_amount})`}
|
||||
checked={checked === item.name}
|
||||
onChange={() => callback(item.name)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Form.Group>
|
||||
</Card.Body>
|
||||
</Card>
|
||||
|
||||
@@ -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<Type.FlagReviewResp>();
|
||||
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 (
|
||||
<Card>
|
||||
<Card.Header>{t('flag_type', { type: 'post' })}</Card.Header>
|
||||
<Card.Header>
|
||||
{object_type !== 'user' ? t('flag_post') : t('flag_user')}
|
||||
</Card.Header>
|
||||
<Card.Body className="p-0">
|
||||
<Alert variant="info" className="border-0 rounded-0 mb-0">
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
gap={1}
|
||||
className="align-items-center mb-2">
|
||||
<BaseUserCard data={submitter_user} avatarSize="24" />
|
||||
{flagItemData?.submit_at && (
|
||||
<FormatTime
|
||||
time={flagItemData.submit_at}
|
||||
className="small text-secondary"
|
||||
preFix={t('proposed')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack className="align-items-start">
|
||||
<p className="mb-0">
|
||||
{object_type !== 'user'
|
||||
? t('flag_post_type', { type: reason?.name })
|
||||
: t('flag_user_type', { type: reason?.name })}
|
||||
</p>
|
||||
</Stack>
|
||||
</Alert>
|
||||
<div className="p-3">
|
||||
<h5 className="mb-3">
|
||||
How do I test weather variable against multiple
|
||||
</h5>
|
||||
{objectType === 'question' && (
|
||||
<div className="mb-4">
|
||||
{tag?.map((item) => {
|
||||
return (
|
||||
<Tag key={item.slug_name} className="me-1" data={item} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<small className="d-block text-secondary mb-4">
|
||||
<span>{t(object_type, { keyPrefix: 'btns' })} </span>
|
||||
<Link to={itemLink} target="_blank" className="link-secondary">
|
||||
#{itemId}
|
||||
</Link>
|
||||
</small>
|
||||
{object_type === 'question' && (
|
||||
<>
|
||||
<h5 className="mb-3">{flagItemData?.title}</h5>
|
||||
<div className="mb-4">
|
||||
{flagItemData?.tags?.map((item) => {
|
||||
return (
|
||||
<Tag key={item.slug_name} className="me-1" data={item} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<div className="small font-monospace">
|
||||
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}
|
||||
</div>
|
||||
<div className="d-flex align-items-center justify-content-between mt-4">
|
||||
<Badge bg="success">normal</Badge>
|
||||
<div className="d-flex flex-wrap align-items-center justify-content-between mt-4">
|
||||
<div>
|
||||
<span
|
||||
className={classNames(
|
||||
'badge',
|
||||
ADMIN_LIST_STATUS[object_status]?.variant,
|
||||
)}>
|
||||
{t(ADMIN_LIST_STATUS[object_status]?.name, {
|
||||
keyPrefix: 'admin.questions',
|
||||
})}
|
||||
</span>
|
||||
{flagItemData?.object_show_status === 2 && (
|
||||
<span
|
||||
className={classNames(
|
||||
'ms-1 badge',
|
||||
ADMIN_LIST_STATUS.unlist.variant,
|
||||
)}>
|
||||
{t(ADMIN_LIST_STATUS.unlist.name, { keyPrefix: 'btns' })}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="d-flex align-items-center small">
|
||||
<BaseUserCard
|
||||
data={{
|
||||
username: 'username',
|
||||
display_name: 'username',
|
||||
avatar: '',
|
||||
reputation: 100,
|
||||
}}
|
||||
avatarSize="24"
|
||||
/>
|
||||
<BaseUserCard data={author_user_info} avatarSize="24" />
|
||||
<FormatTime
|
||||
time={1688107033}
|
||||
time={Number(flagItemData?.created_at)}
|
||||
className="text-secondary ms-1 flex-shrink-0"
|
||||
preFix="answered"
|
||||
preFix={t(itemTimePrefix, { keyPrefix: 'question_detail' })}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-3 d-flex">
|
||||
<Avatar
|
||||
avatar=""
|
||||
size="40"
|
||||
searchStr="s=48"
|
||||
alt=""
|
||||
className="me-2"
|
||||
/>
|
||||
<div className="small">
|
||||
<Link to="/test">
|
||||
111 <span className="text-secondary">@111</span>
|
||||
</Link>
|
||||
<div className="mt-1">
|
||||
I'm a web developer with in-depth experience in UI/UX design.
|
||||
</div>
|
||||
<div className="text-secondary mt-1">280 {t('reputation')}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card.Body>
|
||||
|
||||
<Card.Footer className="p-3">
|
||||
<p>{t('approve_this_type', { type: 'revision' })}</p>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<ApproveDropdown
|
||||
objectType={object_type}
|
||||
itemData={flagItemData}
|
||||
curFilter={ADMIN_LIST_STATUS[object_status]?.name}
|
||||
approveCallback={handlingApprove}
|
||||
/>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
disabled={isLoading}
|
||||
onClick={handleIgnore}>
|
||||
{t('ignore', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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<Type.SuggestReviewResp>();
|
||||
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<string, any> = {};
|
||||
let oldData: Record<string, any> = {};
|
||||
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 (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
{t('suggest_type_edit', {
|
||||
type:
|
||||
type === 'question' || type === 'answer'
|
||||
? t('post_lowercase', { keyPrefix: 'btns' })
|
||||
: type,
|
||||
})}
|
||||
</Card.Header>
|
||||
<Card.Body className="p-0">
|
||||
<Alert variant="info" className="border-0 rounded-0 mb-0">
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
gap={1}
|
||||
className="align-items-center mb-2">
|
||||
<BaseUserCard data={editor} avatarSize="24" />
|
||||
{editTime && (
|
||||
<FormatTime
|
||||
time={editTime}
|
||||
className="small text-secondary"
|
||||
preFix={t('proposed')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack className="align-items-start">
|
||||
<p className="mb-0">{editSummary}</p>
|
||||
</Stack>
|
||||
</Alert>
|
||||
<div className="p-3">
|
||||
<small className="d-block text-secondary mb-4">
|
||||
<span>{t(type, { keyPrefix: 'btns' })} </span>
|
||||
<Link to={itemLink} target="_blank" className="link-secondary">
|
||||
#{itemId}
|
||||
</Link>
|
||||
</small>
|
||||
|
||||
<DiffContent
|
||||
className="mt-2"
|
||||
objectType={type}
|
||||
newData={newData}
|
||||
oldData={oldData}
|
||||
opts={diffOpts}
|
||||
/>
|
||||
</div>
|
||||
</Card.Body>
|
||||
<Card.Footer className="p-3">
|
||||
<p>{t('approve_this_type', { type: 'revision' })}</p>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
disabled={isLoading}
|
||||
onClick={handlingApprove}>
|
||||
{t('approve', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
disabled={isLoading}
|
||||
onClick={handlingReject}>
|
||||
{t('reject', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default Index;
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
+28
-226
@@ -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<Type.ReviewResp>();
|
||||
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<Type.ReviewTypeItem[]>();
|
||||
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<string, any> = {};
|
||||
let oldData: Record<string, any> = {};
|
||||
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 (
|
||||
<Row className="pt-4 mb-5">
|
||||
<h3 className="mb-4">{t('review')}</h3>
|
||||
<Col className="page-main flex-auto">
|
||||
{!noTasks && ro && (
|
||||
<Card>
|
||||
<Card.Header>
|
||||
{t('suggest_type_edit', {
|
||||
type:
|
||||
type === 'question' || type === 'answer'
|
||||
? t('post_lowercase', { keyPrefix: 'btns' })
|
||||
: type,
|
||||
})}
|
||||
</Card.Header>
|
||||
<Card.Body className="p-0">
|
||||
<Alert variant="info" className="border-0 rounded-0 mb-0">
|
||||
<Stack
|
||||
direction="horizontal"
|
||||
gap={1}
|
||||
className="align-items-center mb-2">
|
||||
<BaseUserCard data={editor} avatarSize="24" />
|
||||
{editTime && (
|
||||
<FormatTime
|
||||
time={editTime}
|
||||
className="small text-secondary"
|
||||
preFix={t('proposed')}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
<Stack className="align-items-start">
|
||||
<p className="mb-0">{editSummary}</p>
|
||||
</Stack>
|
||||
</Alert>
|
||||
<div className="p-3">
|
||||
<small className="d-block text-secondary mb-4">
|
||||
<span>{t(type, { keyPrefix: 'btns' })} </span>
|
||||
<Link
|
||||
to={itemLink}
|
||||
target="_blank"
|
||||
className="link-secondary">
|
||||
#{itemId}
|
||||
</Link>
|
||||
</small>
|
||||
|
||||
<DiffContent
|
||||
className="mt-2"
|
||||
objectType={type}
|
||||
newData={newData}
|
||||
oldData={oldData}
|
||||
opts={diffOpts}
|
||||
/>
|
||||
</div>
|
||||
</Card.Body>
|
||||
<Card.Footer className="p-3">
|
||||
<p>{t('approve_this_type', { type: 'revision' })}</p>
|
||||
<Stack direction="horizontal" gap={2}>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
disabled={isLoading}
|
||||
onClick={handlingApprove}>
|
||||
{t('approve', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<ApproveDropdown />
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
disabled={isLoading}
|
||||
onClick={handlingReject}>
|
||||
{t('reject', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
disabled={isLoading}
|
||||
onClick={handlingReject}>
|
||||
{t('ignore', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
disabled={isLoading}
|
||||
onClick={handlingSkip}>
|
||||
{t('skip', { keyPrefix: 'btns' })}
|
||||
</Button>
|
||||
</Stack>
|
||||
</Card.Footer>
|
||||
</Card>
|
||||
)}
|
||||
{noTasks && <Empty>{t('empty')}</Empty>}
|
||||
{currentReviewType === 'suggested_post_edit' && <SuggestEditContent />}
|
||||
|
||||
{/* {currentReviewType === 'flagged_post' && <FlagContent />} */}
|
||||
<FlagContent />
|
||||
<Empty>{t('empty')}</Empty>
|
||||
</Col>
|
||||
|
||||
<Col className="page-right-side mt-4 mt-xl-0">
|
||||
<Filter />
|
||||
<Filter
|
||||
list={reviewTypeList}
|
||||
checked={currentReviewType}
|
||||
callback={(name) => {
|
||||
setCurrentReviewType(name);
|
||||
}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
);
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -29,3 +29,4 @@ export * from './timeline';
|
||||
export * from './revision';
|
||||
export * from './user';
|
||||
export * from './Oauth';
|
||||
export * from './review';
|
||||
|
||||
@@ -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<Type.SuggestReviewResp>(apiUrl);
|
||||
};
|
||||
|
||||
export const getReviewType = () => {
|
||||
return request.get<Type.ReviewTypeItem[]>('/answer/api/v1/reviewing/type');
|
||||
};
|
||||
|
||||
export const getFlagReviewPostList = (page: number) => {
|
||||
const apiUrl = `/answer/api/v1/report/unreviewed/post?page=${page}`;
|
||||
return request.get<Type.FlagReviewResp>(apiUrl);
|
||||
};
|
||||
|
||||
export const putFlagReviewAction = (params: Type.PutFlagReviewParams) => {
|
||||
return request.put('/answer/api/v1/report/review', params);
|
||||
};
|
||||
@@ -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<Type.ReviewResp>(apiUrl);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user