feat: add timeline router

This commit is contained in:
shuai 2022-11-23 15:21:36 +08:00
parent 9359f27729
commit f3ec755e92
15 changed files with 238 additions and 242 deletions

View File

@ -380,6 +380,7 @@ ui:
tag_info:
created_at: Created
edited_at: Edited
history: History
synonyms:
title: Synonyms
text: The following tags will be remapped to

View File

@ -51,6 +51,7 @@ module.exports = {
'react-hooks/exhaustive-deps': 'off',
'react/jsx-props-no-spreading': 'off',
'@typescript-eslint/default-param-last': 'off',
'no-nested-ternary': 'off',
'import/order': [
'error',
{

View File

@ -563,4 +563,6 @@ export const TIMELINE_NORMAL_ACTIVITY_TYPE = [
'deleted',
'downvote',
'upvote',
'reopened',
'closed',
];

View File

@ -376,10 +376,8 @@ export interface AdminDashboard {
}
export interface TimelineReq {
object_type: string;
show_vote: boolean;
object_id?: string;
tag_slug_name?: string;
object_id: string;
}
export interface TimelineItem {

View File

@ -4,28 +4,27 @@ import { Tag } from '@/components';
import { diffText } from '@/utils';
interface Props {
currentData: Record<string, any>;
prevData?: Record<string, any>;
objectType: string;
newData: Record<string, any>;
oldData?: Record<string, any>;
className?: string;
}
const Index: FC<Props> = ({ currentData, prevData, className = '' }) => {
if (!currentData?.content) return null;
const Index: FC<Props> = ({ objectType, newData, oldData, className = '' }) => {
if (!newData?.original_text) return null;
let tag;
if (prevData?.tags) {
const addTags = currentData.tags.filter(
(c) => !prevData?.tags?.find((p) => p.slug_name === c.slug_name),
let tag = newData.tags;
if (objectType === 'question' && oldData?.tags) {
const addTags = newData.tags.filter(
(c) => !oldData?.tags?.find((p) => p.slug_name === c.slug_name),
);
let deleteTags = prevData?.tags
.filter(
(c) => !currentData?.tags.find((p) => p.slug_name === c.slug_name),
)
let deleteTags = oldData?.tags
.filter((c) => !newData?.tags.find((p) => p.slug_name === c.slug_name))
.map((v) => ({ ...v, state: 'delete' }));
deleteTags = deleteTags?.map((v) => {
const index = prevData?.tags?.findIndex(
const index = oldData?.tags?.findIndex(
(c) => c.slug_name === v.slug_name,
);
return {
@ -34,7 +33,7 @@ const Index: FC<Props> = ({ currentData, prevData, className = '' }) => {
};
});
tag = currentData.tags.map((item) => {
tag = newData.tags.map((item) => {
const find = addTags.find((c) => c.slug_name === item.slug_name);
if (find) {
return {
@ -52,27 +51,43 @@ const Index: FC<Props> = ({ currentData, prevData, className = '' }) => {
return (
<div className={className}>
<h5
dangerouslySetInnerHTML={{
__html: diffText(currentData.title, prevData?.title),
}}
className="mb-3"
/>
<div className="mb-4">
{tag.map((item) => {
return (
<Tag
key={item.slug_name}
className="me-1"
data={item}
textClassName={`d-inline-block review-text-${item.state}`}
/>
);
})}
</div>
{objectType !== 'answer' && (
<h5
dangerouslySetInnerHTML={{
__html: diffText(newData.title, oldData?.title),
}}
className="mb-3"
/>
)}
{objectType === 'question' && (
<div className="mb-4">
{tag.map((item) => {
return (
<Tag
key={item.slug_name}
className="me-1"
data={item}
textClassName={`d-inline-block review-text-${item.state}`}
/>
);
})}
</div>
)}
{objectType === 'tag' && (
<div className="mb-4">
{`/tags/${
newData?.main_tag_slug_name
? diffText(
newData.main_tag_slug_name,
oldData?.main_tag_slug_name,
)
: diffText(newData.slug_name, oldData?.slug_name)
}`}
</div>
)}
<div
dangerouslySetInnerHTML={{
__html: diffText(currentData.content, prevData?.content),
__html: diffText(newData.original_text, oldData?.original_text),
}}
className="pre-line"
/>

View File

@ -10,10 +10,19 @@ interface Props {
data: any;
time: number;
preFix: string;
isLogged: boolean;
timelinePath: string;
className?: string;
}
const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
const Index: FC<Props> = ({
data,
time,
preFix,
isLogged,
timelinePath,
className = '',
}) => {
return (
<div className={classnames('d-flex', className)}>
{data?.status !== 'deleted' ? (
@ -62,7 +71,18 @@ const Index: FC<Props> = ({ data, time, preFix, className = '' }) => {
{formatCount(data?.rank)}
</span>
</div>
{time && <FormatTime time={time} preFix={preFix} />}
{time &&
(isLogged ? (
<Link to={timelinePath}>
<FormatTime
time={time}
preFix={preFix}
className="link-secondary"
/>
</Link>
) : (
<FormatTime time={time} preFix={preFix} />
))}
</div>
</div>
);

View File

@ -1,6 +1,7 @@
import { memo, FC, useEffect, useRef } from 'react';
import { Row, Col, Button } from 'react-bootstrap';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
import {
Actions,
@ -22,12 +23,14 @@ interface Props {
/** is author */
isAuthor: boolean;
questionTitle: string;
isLogged: boolean;
callback: (type: string) => void;
}
const Index: FC<Props> = ({
aid,
data,
isAuthor,
isLogged,
questionTitle = '',
callback,
}) => {
@ -119,7 +122,17 @@ const Index: FC<Props> = ({
data={data?.update_user_info}
time={Number(data.update_time)}
preFix={t('edit')}
isLogged={isLogged}
timelinePath={`/posts/${data.question_id}/${data.id}/timeline`}
/>
) : isLogged ? (
<Link to={`/posts/${data.question_id}/${data.id}/timeline`}>
<FormatTime
time={Number(data.update_time)}
preFix={t('edit')}
className="link-secondary fs-14"
/>
</Link>
) : (
<FormatTime
time={Number(data.update_time)}
@ -133,6 +146,8 @@ const Index: FC<Props> = ({
data={data?.user_info}
time={Number(data.create_time)}
preFix={t('answered')}
isLogged={isLogged}
timelinePath={`/posts/${data.question_id}/${data.id}/timeline`}
/>
</Col>
</Row>

View File

@ -18,10 +18,11 @@ import { following } from '@/services';
interface Props {
data: any;
hasAnswer: boolean;
isLogged: boolean;
initPage: (type: string) => void;
}
const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
const { t } = useTranslation('translation', {
keyPrefix: 'question_detail',
});
@ -133,7 +134,17 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
data={data?.user_info}
time={data.edit_time}
preFix={t('edit')}
isLogged={isLogged}
timelinePath={`/posts/${data.id}/timeline`}
/>
) : isLogged ? (
<Link to={`/posts/${data.id}/timeline`}>
<FormatTime
time={data.edit_time}
preFix={t('edit')}
className="link-secondary fs-14"
/>
</Link>
) : (
<FormatTime
time={data.edit_time}
@ -147,6 +158,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer }) => {
data={data?.user_info}
time={data.create_time}
preFix={t('asked')}
isLogged={isLogged}
timelinePath={`/posts/${data.id}/timeline`}
/>
</Col>
</Row>

View File

@ -39,6 +39,7 @@ const Index = () => {
const { setUsers } = usePageUsers();
const userInfo = loggedUserInfoStore((state) => state.user);
const isAuthor = userInfo?.username === question?.user_info?.username;
const isLogged = Boolean(userInfo?.access_token);
const requestAnswers = async () => {
const res = await getAnswers({
order: order === 'updated' ? order : 'default',
@ -126,6 +127,7 @@ const Index = () => {
data={question}
initPage={initPage}
hasAnswer={answers.count > 0}
isLogged={isLogged}
/>
{answers.count > 0 && (
<>
@ -139,6 +141,7 @@ const Index = () => {
questionTitle={question?.title || ''}
isAuthor={isAuthor}
callback={initPage}
isLogged={isLogged}
/>
);
})}

View File

@ -12,8 +12,11 @@ import {
saveSynonymsTags,
deleteTag,
} from '@/services';
import { loggedUserInfoStore } from '@/stores';
const TagIntroduction = () => {
const userInfo = loggedUserInfoStore((state) => state.user);
const isLogged = Boolean(userInfo?.access_token);
const [isEdit, setEditState] = useState(false);
const { tagName } = useParams();
const { data: tagInfo } = useTagInfo({ name: tagName });
@ -125,6 +128,13 @@ const TagIntroduction = () => {
</Button>
);
})}
{isLogged && (
<Link
to={`/tags/${tagInfo?.tag_id}/timeline`}
className="link-secondary btn-no-border p-0 fs-14 ms-3">
{t('history')}
</Link>
)}
</div>
</Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0">

View File

@ -6,113 +6,43 @@ import { useTranslation } from 'react-i18next';
import { Icon, BaseUserCard, DiffContent, FormatTime } from '@/components';
import { TIMELINE_NORMAL_ACTIVITY_TYPE } from '@/common/constants';
import * as Type from '@/common/interface';
const data1 = {
title: '不是管理员提一个问题看看能不能编辑resserved tag?',
tags: [
{
display_name: 'bug',
slug_name: 'bug',
recommend: true,
reserved: false,
},
{
display_name: '黄马甲',
slug_name: '黄马甲',
recommend: false,
reserved: true,
},
{
display_name: 'go',
slug_name: 'go',
recommend: false,
reserved: false,
},
],
content: `# 前言
Promise
## Promise
Promise CommonJS ES6 Promise ES6 BluebirdQ
Promise
Promise3-2-1
pendingfulfilledrejected
* pending fulfilledresolve
* pending rejectedreject
then
catch Promise.all/race/allSettled`,
};
const data2 = {
title: '提一个问题看看能不能编辑 resserved tag?',
tags: [
{
display_name: 'discussion',
slug_name: 'discussion',
recommend: true,
reserved: false,
},
{
display_name: '黄马甲',
slug_name: '黄马甲',
recommend: false,
reserved: true,
},
{
display_name: 'go',
slug_name: 'go',
recommend: false,
reserved: false,
},
],
content: `# 前言
Promise
## titlte
## Promise
Promise CommonJS ES6 Promise ES6 BluebirdQ
Promise
Promise3-2-1
* pending fulfilledresolve
* pending rejectedreject
then
`,
};
import { getTimelineDetail } from '@/services';
interface Props {
data: Type.TimelineItem;
objectInfo: Type.TimelineObject;
source: 'question' | 'answer' | 'tag';
isAdmin: boolean;
revisionList: Type.TimelineItem[];
}
const Index: FC<Props> = ({
data,
isAdmin,
source = 'question',
objectInfo,
}) => {
const Index: FC<Props> = ({ data, isAdmin, objectInfo, revisionList }) => {
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
const [isOpen, setIsOpen] = useState(false);
const handleItemClick = () => {
const [detailData, setDetailData] = useState({
new_revision: {},
old_revision: {},
});
const handleItemClick = async (id) => {
if (!isOpen) {
const revisionItem = revisionList?.find((v) => v.revision_id === id);
let oldId;
if (revisionList?.length > 0 && revisionItem) {
const idIndex = revisionList.indexOf(revisionItem) || 0;
if (idIndex === revisionList.length - 1) {
oldId = 0;
} else {
oldId = revisionList[idIndex + 1].revision_id;
}
}
const res = await getTimelineDetail({
new_revision_id: id,
old_revision_id: oldId,
});
setDetailData(res);
}
setIsOpen(!isOpen);
};
return (
<>
<tr>
@ -126,9 +56,10 @@ const Index: FC<Props> = ({
data.activity_type === 'edited' ||
data.activity_type === 'asked' ||
data.activity_type === 'created' ||
(source === 'answer' && data.activity_type === 'answered')) && (
(objectInfo.object_type === 'answer' &&
data.activity_type === 'answered')) && (
<Button
onClick={handleItemClick}
onClick={() => handleItemClick(data.revision_id)}
variant="link"
className="text-body p-0 btn-no-border">
<Icon
@ -139,24 +70,25 @@ const Index: FC<Props> = ({
</Button>
)}
{data.activity_type === 'accept' && (
<Link to={`/question/${objectInfo.question_id}`}>
<Link to={`/questions/${objectInfo.question_id}`}>
{t(data.activity_type)}
</Link>
)}
{source === 'question' && data.activity_type === 'answered' && (
<Link
to={`/question/${objectInfo.question_id}/${objectInfo.answer_id}`}>
{t(data.activity_type)}
</Link>
)}
{data.object_type === 'question' &&
data.activity_type === 'answered' && (
<Link
to={`/questions/${objectInfo.question_id}/${data.object_id}`}>
{t(data.activity_type)}
</Link>
)}
{data.activity_type === 'commented' && (
<Link
to={
data.object_type === 'answer'
? `/question/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}`
: `/question/${objectInfo.question_id}?commentId=${data.object_id}`
objectInfo.object_type === 'answer'
? `/questions/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}`
: `/questions/${objectInfo.question_id}?commentId=${data.object_id}`
}>
{t(data.activity_type)}
</Link>
@ -167,7 +99,7 @@ const Index: FC<Props> = ({
)}
{data.cancelled && (
<div className="text-danger"> {t('cancelled')}</div>
<div className="text-danger">{t('cancelled')}</div>
)}
</td>
<td>
@ -192,7 +124,11 @@ const Index: FC<Props> = ({
<td colSpan={5} className="p-0 py-5">
<Row className="justify-content-center">
<Col xxl={8}>
<DiffContent currentData={data1} prevData={data2} />
<DiffContent
objectType={objectInfo.object_type}
newData={detailData?.new_revision}
oldData={detailData?.old_revision}
/>
</Col>
</Row>
</td>

View File

@ -1,113 +1,70 @@
import { FC } from 'react';
import { FC, useState } from 'react';
import { Container, Row, Col, Form, Table } from 'react-bootstrap';
import { Link } from 'react-router-dom';
import { Link, useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { loggedUserInfoStore } from '@/stores';
import { useTimelineData } from '@/services';
import { PageTitle } from '@/components';
import HistoryItem from './components/Item';
// const list = [
// {
// activity_id: 1,
// revision_id: 1,
// created_at: 1669084579,
// activity_type: 'deleted',
// username: 'John Doe',
// user_display_name: 'John Doe',
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
// object_id: '1',
// object_type: 'question',
// cancelled: false,
// cancelled_at: null,
// },
// {
// activity_id: 2,
// revision_id: 2,
// created_at: 1669084579,
// activity_type: 'undeleted',
// username: 'John Doe2',
// user_display_name: 'John Doe2',
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
// object_id: '2',
// object_type: 'question',
// cancelled: false,
// cancelled_at: null,
// },
// {
// activity_id: 3,
// revision_id: 3,
// created_at: 1669084579,
// activity_type: 'downvote',
// username: 'johndoe3',
// user_display_name: 'John Doe3',
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
// object_id: '3',
// object_type: 'question',
// cancelled: true,
// cancelled_at: 1637021579,
// },
// {
// activity_id: 4,
// revision_id: 4,
// created_at: 1669084579,
// activity_type: 'rollback',
// username: 'johndoe4',
// user_display_name: 'John Doe4',
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
// object_id: '4',
// object_type: 'question',
// cancelled: false,
// cancelled_at: null,
// },
// {
// activity_id: 5,
// revision_id: 5,
// created_at: 1669084579,
// activity_type: 'edited',
// username: 'johndoe4',
// user_display_name: 'John Doe4',
// object_id: '5',
// object_type: 'question',
// comment: '啊撒旦法师打发房管局挥洒过短发合计干哈就撒刚发几哈',
// cancelled: false,
// cancelled_at: null,
// },
// ];
// const object_info = {
// title: '问题标题,当回答时也是问题标题,当为 tag 时是 slug_name',
// object_type: 'question', // question/answer/tag
// question_id: 'xxxxxxxxxxxxxxxxxxx',
// answer_id: 'xxxxxxxxxxxxxxxx',
// };
const Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
const { qid = '', aid = '', tid = '' } = useParams();
const { is_admin } = loggedUserInfoStore((state) => state.user);
const [showVotes, setShowVotes] = useState(false);
const { data: timelineData } = useTimelineData({
object_id: '10010000000000001',
object_type: 'question',
show_vote: false,
object_id: tid || aid || qid,
show_vote: showVotes,
});
console.log('timelineData=', timelineData);
const handleSwitch = (bol: boolean) => {
setShowVotes(bol);
};
let linkUrl = '';
if (timelineData?.object_info.object_type === 'question') {
linkUrl = `/questions/${timelineData?.object_info.question_id}`;
}
if (timelineData?.object_info.object_type === 'answer') {
linkUrl = `/questions/${timelineData?.object_info.question_id}/${timelineData?.object_info.answer_id}`;
}
if (timelineData?.object_info.object_type === 'tag') {
linkUrl = `/tags/${timelineData?.object_info.title}`;
}
const revisionList =
timelineData?.timeline?.filter((item) => item.revision_id > 0) || [];
return (
<Container className="py-3">
<PageTitle
title={
timelineData?.object_info.object_type === 'tag'
? `Timeline for tag ${timelineData?.object_info.title}`
: `Timeline for ${timelineData?.object_info.title}`
}
/>
<Row className="py-3 justify-content-center">
<Col xxl={10}>
<h5 className="mb-4">
{t('title')} <Link to="/">{timelineData?.object_info?.title}</Link>
{t('title')}{' '}
<Link to={linkUrl}>{timelineData?.object_info?.title}</Link>
</h5>
<Form.Check
className="mb-4"
type="switch"
id="custom-switch"
label={t('show_votes')}
/>
{timelineData?.object_info.object_type !== 'tag' && (
<Form.Check
className="mb-4"
type="switch"
id="custom-switch"
label={t('show_votes')}
checked={showVotes}
onChange={(e) => handleSwitch(e.target.checked)}
/>
)}
<Table hover>
<thead>
<tr>
@ -123,9 +80,9 @@ const Index: FC = () => {
<HistoryItem
data={item}
objectInfo={timelineData?.object_info}
key={item.revision_id}
key={item.activity_id}
isAdmin={is_admin}
source="question"
revisionList={revisionList}
/>
);
})}

View File

@ -202,7 +202,21 @@ const routes: RouteNode[] = [
},
},
{
path: '/history',
path: '/posts/:qid/timeline',
page: 'pages/Timeline',
guard: async () => {
return guard.logged();
},
},
{
path: '/posts/:qid/:aid/timeline',
page: 'pages/Timeline',
guard: async () => {
return guard.logged();
},
},
{
path: '/tags/:tid/timeline',
page: 'pages/Timeline',
guard: async () => {
return guard.logged();

View File

@ -17,3 +17,14 @@ export const useTimelineData = (params: Type.TimelineReq) => {
mutate,
};
};
export const getTimelineDetail = (params: {
new_revision_id: string;
old_revision_id: string;
}) => {
return request.get(
`/answer/api/v1/activity/timeline/detail?${qs.stringify(params, {
skipNulls: true,
})}`,
);
};

View File

@ -176,7 +176,7 @@ function diffText(newText: string, oldText: string): string {
?.replace(/<iframe/gi, '&lt;iframe')
?.replace(/<input/gi, '&lt;input');
}
const diff = Diff.diffChars(newText, oldText);
const diff = Diff.diffChars(oldText, newText);
const result = diff.map((part) => {
if (part.added) {
return `<span class="review-text-add">${part.value}</span>`;