From 079513e8b6f11969bfd0f228be7334cf2f179e2c Mon Sep 17 00:00:00 2001 From: "haitao(lj)" Date: Fri, 25 Nov 2022 17:55:13 +0800 Subject: [PATCH] feat(review): ready for testing --- i18n/en_US.yaml | 3 + ui/src/common/interface.ts | 34 ++- ui/src/components/DiffContent/index.tsx | 21 +- .../Header/components/NavItems/index.tsx | 13 +- ui/src/components/Operate/index.tsx | 12 +- ui/src/components/Tag/index.tsx | 5 +- ui/src/pages/Review/index.tsx | 249 ++++++++++++++---- ui/src/pages/Search/components/Head/index.tsx | 5 +- ui/src/pages/Tags/Detail/index.tsx | 9 +- ui/src/pages/Tags/Edit/index.tsx | 4 +- ui/src/pages/Tags/Info/index.tsx | 12 +- ui/src/router/pathFactory.ts | 28 ++ ui/src/router/routes.ts | 1 - ui/src/services/client/index.ts | 1 + ui/src/services/client/notification.ts | 2 +- ui/src/services/client/revision.ts | 31 +++ 16 files changed, 358 insertions(+), 72 deletions(-) create mode 100644 ui/src/router/pathFactory.ts create mode 100644 ui/src/services/client/revision.ts diff --git a/i18n/en_US.yaml b/i18n/en_US.yaml index e9b98f40..0f1a7311 100644 --- a/i18n/en_US.yaml +++ b/i18n/en_US.yaml @@ -1190,6 +1190,9 @@ ui: answer_edit: Answer edit tag_edit: Tag edit edit_summary: Edit summary + edit_question: Edit question + edit_answer: Edit answer + edit_tag: Edit tag empty: No review tasks left. timeline: undeleted: undeleted diff --git a/ui/src/common/interface.ts b/ui/src/common/interface.ts index 4d0d9bb3..4d6de991 100644 --- a/ui/src/common/interface.ts +++ b/ui/src/common/interface.ts @@ -157,6 +157,13 @@ export interface SetNoticeReq { notice_switch: boolean; } +export interface NotificationStatus { + inbox: number; + achievement: number; + revision: number; + can_revision: boolean; +} + export interface QuestionDetailRes { id: string; title: string; @@ -192,7 +199,6 @@ export interface AnswerItem { create_time: string; update_time: string; user_info: UserInfoBase; - [prop: string]: any; } @@ -406,3 +412,29 @@ export interface TimelineRes { object_info: TimelineObject; timeline: TimelineItem[]; } + +export interface ReviewItem { + type: 'question' | 'answer' | 'tag'; + info: { + object_id: string; + title: string; + content: string; + html: string; + tags: Tag[]; + }; + unreviewed_info: { + id: string; + use_id: string; + object_id: string; + title: string; + status: 0 | 1; + create_at: number; + user_info: UserInfoBase; + reason: string; + content: Tag | QuestionDetailRes | AnswerItem; + }; +} +export interface ReviewResp { + count: number; + list: ReviewItem[]; +} diff --git a/ui/src/components/DiffContent/index.tsx b/ui/src/components/DiffContent/index.tsx index 77386664..36f87de2 100644 --- a/ui/src/components/DiffContent/index.tsx +++ b/ui/src/components/DiffContent/index.tsx @@ -4,13 +4,26 @@ import { Tag } from '@/components'; import { diffText } from '@/utils'; interface Props { - objectType: string; + objectType: string | 'question' | 'answer' | 'tag'; newData: Record; oldData?: Record; className?: string; + opts?: Partial<{ + showTitle: boolean; + showTagUrlSlug: boolean; + }>; } -const Index: FC = ({ objectType, newData, oldData, className = '' }) => { +const Index: FC = ({ + objectType, + newData, + oldData, + className = '', + opts = { + showTitle: true, + showTagUrlSlug: true, + }, +}) => { if (!newData?.original_text) return null; let tag = newData.tags; @@ -51,7 +64,7 @@ const Index: FC = ({ objectType, newData, oldData, className = '' }) => { return (
- {objectType !== 'answer' && ( + {objectType !== 'answer' && opts?.showTitle && (
= ({ objectType, newData, oldData, className = '' }) => { })}
)} - {objectType === 'tag' && ( + {objectType === 'tag' && opts?.showTagUrlSlug && (
{`/tags/${ newData?.main_tag_slug_name diff --git a/ui/src/components/Header/components/NavItems/index.tsx b/ui/src/components/Header/components/NavItems/index.tsx index 147d3d58..17a3a683 100644 --- a/ui/src/components/Header/components/NavItems/index.tsx +++ b/ui/src/components/Header/components/NavItems/index.tsx @@ -3,10 +3,11 @@ import { Nav, Dropdown } from 'react-bootstrap'; import { useTranslation } from 'react-i18next'; import { Link, NavLink } from 'react-router-dom'; +import type * as Type from '@/common/interface'; import { Avatar, Icon } from '@/components'; interface Props { - redDot; + redDot: Type.NotificationStatus | undefined; userInfo; logOut: () => void; } @@ -56,10 +57,14 @@ const Index: FC = ({ redDot, userInfo, logOut }) => { {userInfo?.is_admin ? ( {t('header.nav.admin')} ) : null} - {/* TODO: use review permission */} - {userInfo?.is_admin ? ( - + {redDot?.can_revision ? ( + {t('header.nav.review')} + {redDot?.revision > 0 && ( + + New Review + + )} ) : null} diff --git a/ui/src/components/Operate/index.tsx b/ui/src/components/Operate/index.tsx index cdacc751..da27b7f4 100644 --- a/ui/src/components/Operate/index.tsx +++ b/ui/src/components/Operate/index.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import { Modal } from '@/components'; import { useReportModal, useToast } from '@/hooks'; import Share from '../Share'; -import { deleteQuestion, deleteAnswer } from '@/services'; +import { deleteQuestion, deleteAnswer, editCheck } from '@/services'; import { tryNormalLogged } from '@/utils/guard'; interface IProps { @@ -96,6 +96,15 @@ const Index: FC = ({ }); } }; + const handleEdit = async (evt) => { + let checkObjectId = qid; + if (type === 'answer') { + checkObjectId = aid; + } + editCheck(checkObjectId).catch(() => { + evt.preventDefault(); + }); + }; const handleAction = (action) => { if (!tryNormalLogged(true)) { @@ -124,6 +133,7 @@ const Index: FC = ({ key={item.action} to={editUrl} className="link-secondary p-0 fs-14 me-3" + onClick={handleEdit} style={{ lineHeight: '23px' }}> {item.name} diff --git a/ui/src/components/Tag/index.tsx b/ui/src/components/Tag/index.tsx index 2891232f..64e9f25b 100644 --- a/ui/src/components/Tag/index.tsx +++ b/ui/src/components/Tag/index.tsx @@ -3,6 +3,7 @@ import React, { memo, FC } from 'react'; import classNames from 'classnames'; import { Tag } from '@/common/interface'; +import { pathFactory } from '@/router/pathFactory'; interface IProps { data: Tag; @@ -17,9 +18,7 @@ const Index: FC = ({ className = '', textClassName = '', }) => { - href ||= `/tags/${encodeURIComponent( - data.main_tag_slug_name || data.slug_name, - )}`.toLowerCase(); + href ||= pathFactory.tagLanding(data); return ( { const { t } = useTranslation('translation', { keyPrefix: 'page_review' }); + const [isLoading, setIsLoading] = useState(false); + const [noTasks, setNoTasks] = useState(false); + const [page, setPage] = useState(1); + const { data: reviewResp, mutate: mutateList } = useReviewList(page); + const ro = reviewResp?.list[0]; + const { info, type, unreviewed_info } = ro || { + info: null, + type: '', + unreviewed_info: null, + }; + const reviewInfo = unreviewed_info?.content; + const mutateNextPage = () => { + const count = reviewResp?.count; + if (count && page < count) { + setPage(page + 1); + } else { + setNoTasks(true); + } + }; + const handlingSkip = () => { + mutateNextPage(); + }; + const handlingApprove = () => { + if (!unreviewed_info) { + return; + } + setIsLoading(true); + revisionAudit(unreviewed_info.id, 'approve') + .then(() => { + mutateList(); + }) + .catch((ex) => { + console.log('ex: ', ex); + }) + .finally(() => { + setIsLoading(false); + }); + }; + const handlingReject = () => { + if (!unreviewed_info) { + return; + } + setIsLoading(true); + revisionAudit(unreviewed_info.id, 'reject') + .then(() => { + mutateList(); + }) + .catch((ex) => { + console.log('ex: ', ex); + }) + .finally(() => { + setIsLoading(false); + }); + }; - const { user } = loggedUserInfoStore.getState(); + let itemLink = ''; + let itemTitle = ''; + let editBadge = ''; + 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); + itemTitle = info?.title; + editBadge = t('question_edit'); + editSummary ||= t('edit_question'); + } else if (type === 'answer') { + itemLink = pathFactory.answerLanding( + // @ts-ignore + unreviewed_info.content.question_id, + unreviewed_info.object_id, + ); + itemTitle = info?.title; + editBadge = t('answer_edit'); + editSummary ||= t('edit_answer'); + } else if (type === 'tag') { + const tagInfo = unreviewed_info.content as Type.Tag; + itemLink = pathFactory.tagLanding(tagInfo); + itemTitle = tagInfo.display_name; + editBadge = t('tag_edit'); + editSummary ||= t('edit_tag'); + } + useEffect(() => { + if (!reviewResp) { + return; + } + window.scrollTo({ top: 0 }); + if (!reviewResp.list || !reviewResp.list.length) { + setNoTasks(true); + } + }, [reviewResp]); return (

{t('review')}

- - - - {t('question_edit')} - - - How do I test whether variable against multiple - -

- {t('edit_summary')}: Editing part of the code and correcting the - grammar. -

-
- - - - -
- - Content - - - - - - - - - {t('empty')} + {!noTasks && ro && ( + <> + + + + + {editBadge} + + + {itemTitle} + +

+ {t('edit_summary')}: {editSummary} +

+
+ + + {editTime && ( + + )} + +
+ + + {type === 'question' && + info && + reviewInfo && + 'content' in reviewInfo && ( + + )} + {type === 'answer' && + info && + reviewInfo && + 'content' in reviewInfo && ( + + )} + {type === 'tag' && info && reviewInfo && ( + + )} + + + + + + + + + + )} + {noTasks && ( + + {t('empty')} + + )}
); diff --git a/ui/src/pages/Search/components/Head/index.tsx b/ui/src/pages/Search/components/Head/index.tsx index 260ecc65..ec1d1dc4 100644 --- a/ui/src/pages/Search/components/Head/index.tsx +++ b/ui/src/pages/Search/components/Head/index.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import { following } from '@/services'; import { tryNormalLogged } from '@/utils/guard'; import { escapeRemove } from '@/utils'; +import { pathFactory } from '@/router/pathFactory'; interface Props { data; @@ -53,7 +54,9 @@ const Index: FC = ({ data }) => { {data.excerpt && (

{escapeRemove(data.excerpt)} - [{t('more')}] + + [{t('more')}] +

)} diff --git a/ui/src/pages/Tags/Detail/index.tsx b/ui/src/pages/Tags/Detail/index.tsx index c49e1ebd..1c32621a 100644 --- a/ui/src/pages/Tags/Detail/index.tsx +++ b/ui/src/pages/Tags/Detail/index.tsx @@ -9,6 +9,7 @@ import { useTagInfo, useFollow } from '@/services'; import QuestionList from '@/components/QuestionList'; import HotQuestions from '@/components/HotQuestions'; import { escapeRemove } from '@/utils'; +import { pathFactory } from '@/router/pathFactory'; const Questions: FC = () => { const { t } = useTranslation('translation', { keyPrefix: 'tags' }); @@ -30,7 +31,7 @@ const Questions: FC = () => { if (tagResp) { const info = { ...tagResp }; if (info.main_tag_slug_name) { - navigate(`/tags/${info.main_tag_slug_name}`, { replace: true }); + navigate(pathFactory.tagLanding(info), { replace: true }); return; } if (followResp) { @@ -62,7 +63,7 @@ const Questions: FC = () => {

{tagInfo.display_name} @@ -71,9 +72,7 @@ const Questions: FC = () => {

{escapeRemove(tagInfo.excerpt) || t('no_description')} - + [{t('more')}]

diff --git a/ui/src/pages/Tags/Edit/index.tsx b/ui/src/pages/Tags/Edit/index.tsx index e01225e7..c3612768 100644 --- a/ui/src/pages/Tags/Edit/index.tsx +++ b/ui/src/pages/Tags/Edit/index.tsx @@ -175,7 +175,7 @@ const Ask = () => { @@ -188,7 +188,7 @@ const Ask = () => { diff --git a/ui/src/pages/Tags/Info/index.tsx b/ui/src/pages/Tags/Info/index.tsx index 6477030a..f9430280 100644 --- a/ui/src/pages/Tags/Info/index.tsx +++ b/ui/src/pages/Tags/Info/index.tsx @@ -11,7 +11,9 @@ import { useQuerySynonymsTags, saveSynonymsTags, deleteTag, + editCheck, } from '@/services'; +import { pathFactory } from '@/router/pathFactory'; import { loggedUserInfoStore } from '@/stores'; const TagIntroduction = () => { @@ -27,7 +29,9 @@ const TagIntroduction = () => { return null; } if (tagInfo.main_tag_slug_name) { - navigate(`/tags/${tagInfo.main_tag_slug_name}/info`, { replace: true }); + navigate(pathFactory.tagInfo(tagInfo.main_tag_slug_name), { + replace: true, + }); return null; } const handleEdit = () => { @@ -51,7 +55,9 @@ const TagIntroduction = () => { }; const handleEditTag = () => { - navigate(`/tags/${tagInfo?.tag_id}/edit`); + editCheck(tagInfo?.tag_id).then(() => { + navigate(pathFactory.tagEdit(tagInfo?.tag_id)); + }); }; const handleDeleteTag = () => { if (synonymsTags && synonymsTags.length > 0) { @@ -93,7 +99,7 @@ const TagIntroduction = () => {

{tagInfo.display_name} diff --git a/ui/src/router/pathFactory.ts b/ui/src/router/pathFactory.ts new file mode 100644 index 00000000..6d2725ce --- /dev/null +++ b/ui/src/router/pathFactory.ts @@ -0,0 +1,28 @@ +import type * as Type from '@/common/interface'; + +const tagLanding = (tag: Type.Tag) => { + let slugName = tag.main_tag_slug_name || tag.slug_name || ''; + slugName = slugName.toLowerCase(); + return `/tags/${encodeURIComponent(slugName)}`; +}; +const tagInfo = (slugName: string) => { + slugName = slugName.toLowerCase(); + return `/tags/${encodeURIComponent(slugName)}/info`; +}; +const tagEdit = (tagId: string) => { + return `/tags/${tagId}/edit`; +}; +const questionLanding = (question_id: string) => { + return `/questions/${question_id}`; +}; +const answerLanding = (question_id: string, answer_id: string) => { + return `/questions/${question_id}/${answer_id}`; +}; + +export const pathFactory = { + tagLanding, + tagInfo, + tagEdit, + questionLanding, + answerLanding, +}; diff --git a/ui/src/router/routes.ts b/ui/src/router/routes.ts index d797be8b..04daf448 100644 --- a/ui/src/router/routes.ts +++ b/ui/src/router/routes.ts @@ -192,7 +192,6 @@ const routes: RouteNode[] = [ { path: '/users/confirm-new-email', page: 'pages/Users/ConfirmNewEmail', - // TODO: guard this }, { path: '/users/account-suspended', diff --git a/ui/src/services/client/index.ts b/ui/src/services/client/index.ts index a9b3b8b3..33756e76 100644 --- a/ui/src/services/client/index.ts +++ b/ui/src/services/client/index.ts @@ -7,3 +7,4 @@ export * from './tag'; export * from './settings'; export * from './legal'; export * from './timeline'; +export * from './revision'; diff --git a/ui/src/services/client/notification.ts b/ui/src/services/client/notification.ts index c4e535c8..b499bbda 100644 --- a/ui/src/services/client/notification.ts +++ b/ui/src/services/client/notification.ts @@ -32,7 +32,7 @@ export const readNotification = (id) => { export const useQueryNotificationStatus = () => { const apiUrl = '/answer/api/v1/notification/status'; - return useSWR<{ inbox: number; achievement: number }>( + return useSWR( tryLoggedAndActivated().ok ? apiUrl : null, request.instance.get, { diff --git a/ui/src/services/client/revision.ts b/ui/src/services/client/revision.ts new file mode 100644 index 00000000..28110ae9 --- /dev/null +++ b/ui/src/services/client/revision.ts @@ -0,0 +1,31 @@ +import useSWR from 'swr'; + +import request from '@/utils/request'; +import * as Type from '@/common/interface'; + +export const editCheck = (id: string) => { + const apiUrl = `/answer/api/v1/revisions/edit/check?id=${id}`; + return request.get(apiUrl); +}; + +export const revisionAudit = (id: string, operation: 'approve' | 'reject') => { + const apiUrl = `/answer/api/v1/revisions/audit`; + return request.put(apiUrl, { + id, + operation, + }); +}; + +export const useReviewList = (page: number) => { + const apiUrl = `/answer/api/v1/revisions/unreviewed?page=${page}`; + const { data, error, mutate } = useSWR( + apiUrl, + request.instance.get, + ); + return { + data, + isLoading: !data && !error, + error, + mutate, + }; +};