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: tag_info:
created_at: Created created_at: Created
edited_at: Edited edited_at: Edited
history: History
synonyms: synonyms:
title: Synonyms title: Synonyms
text: The following tags will be remapped to text: The following tags will be remapped to

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,8 +12,11 @@ import {
saveSynonymsTags, saveSynonymsTags,
deleteTag, deleteTag,
} from '@/services'; } from '@/services';
import { loggedUserInfoStore } from '@/stores';
const TagIntroduction = () => { const TagIntroduction = () => {
const userInfo = loggedUserInfoStore((state) => state.user);
const isLogged = Boolean(userInfo?.access_token);
const [isEdit, setEditState] = useState(false); const [isEdit, setEditState] = useState(false);
const { tagName } = useParams(); const { tagName } = useParams();
const { data: tagInfo } = useTagInfo({ name: tagName }); const { data: tagInfo } = useTagInfo({ name: tagName });
@ -125,6 +128,13 @@ const TagIntroduction = () => {
</Button> </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> </div>
</Col> </Col>
<Col xxl={3} lg={4} sm={12} className="mt-5 mt-lg-0"> <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 { Icon, BaseUserCard, DiffContent, FormatTime } from '@/components';
import { TIMELINE_NORMAL_ACTIVITY_TYPE } from '@/common/constants'; import { TIMELINE_NORMAL_ACTIVITY_TYPE } from '@/common/constants';
import * as Type from '@/common/interface'; import * as Type from '@/common/interface';
import { getTimelineDetail } from '@/services';
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
`,
};
interface Props { interface Props {
data: Type.TimelineItem; data: Type.TimelineItem;
objectInfo: Type.TimelineObject; objectInfo: Type.TimelineObject;
source: 'question' | 'answer' | 'tag';
isAdmin: boolean; isAdmin: boolean;
revisionList: Type.TimelineItem[];
} }
const Index: FC<Props> = ({ const Index: FC<Props> = ({ data, isAdmin, objectInfo, revisionList }) => {
data,
isAdmin,
source = 'question',
objectInfo,
}) => {
const { t } = useTranslation('translation', { keyPrefix: 'timeline' }); const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
const [isOpen, setIsOpen] = useState(false); 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); setIsOpen(!isOpen);
}; };
return ( return (
<> <>
<tr> <tr>
@ -126,9 +56,10 @@ const Index: FC<Props> = ({
data.activity_type === 'edited' || data.activity_type === 'edited' ||
data.activity_type === 'asked' || data.activity_type === 'asked' ||
data.activity_type === 'created' || data.activity_type === 'created' ||
(source === 'answer' && data.activity_type === 'answered')) && ( (objectInfo.object_type === 'answer' &&
data.activity_type === 'answered')) && (
<Button <Button
onClick={handleItemClick} onClick={() => handleItemClick(data.revision_id)}
variant="link" variant="link"
className="text-body p-0 btn-no-border"> className="text-body p-0 btn-no-border">
<Icon <Icon
@ -139,24 +70,25 @@ const Index: FC<Props> = ({
</Button> </Button>
)} )}
{data.activity_type === 'accept' && ( {data.activity_type === 'accept' && (
<Link to={`/question/${objectInfo.question_id}`}> <Link to={`/questions/${objectInfo.question_id}`}>
{t(data.activity_type)} {t(data.activity_type)}
</Link> </Link>
)} )}
{source === 'question' && data.activity_type === 'answered' && ( {data.object_type === 'question' &&
<Link data.activity_type === 'answered' && (
to={`/question/${objectInfo.question_id}/${objectInfo.answer_id}`}> <Link
{t(data.activity_type)} to={`/questions/${objectInfo.question_id}/${data.object_id}`}>
</Link> {t(data.activity_type)}
)} </Link>
)}
{data.activity_type === 'commented' && ( {data.activity_type === 'commented' && (
<Link <Link
to={ to={
data.object_type === 'answer' objectInfo.object_type === 'answer'
? `/question/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}` ? `/questions/${objectInfo.question_id}/${objectInfo.answer_id}?commentId=${data.object_id}`
: `/question/${objectInfo.question_id}?commentId=${data.object_id}` : `/questions/${objectInfo.question_id}?commentId=${data.object_id}`
}> }>
{t(data.activity_type)} {t(data.activity_type)}
</Link> </Link>
@ -167,7 +99,7 @@ const Index: FC<Props> = ({
)} )}
{data.cancelled && ( {data.cancelled && (
<div className="text-danger"> {t('cancelled')}</div> <div className="text-danger">{t('cancelled')}</div>
)} )}
</td> </td>
<td> <td>
@ -192,7 +124,11 @@ const Index: FC<Props> = ({
<td colSpan={5} className="p-0 py-5"> <td colSpan={5} className="p-0 py-5">
<Row className="justify-content-center"> <Row className="justify-content-center">
<Col xxl={8}> <Col xxl={8}>
<DiffContent currentData={data1} prevData={data2} /> <DiffContent
objectType={objectInfo.object_type}
newData={detailData?.new_revision}
oldData={detailData?.old_revision}
/>
</Col> </Col>
</Row> </Row>
</td> </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 { 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 { useTranslation } from 'react-i18next';
import { loggedUserInfoStore } from '@/stores'; import { loggedUserInfoStore } from '@/stores';
import { useTimelineData } from '@/services'; import { useTimelineData } from '@/services';
import { PageTitle } from '@/components';
import HistoryItem from './components/Item'; 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 Index: FC = () => {
const { t } = useTranslation('translation', { keyPrefix: 'timeline' }); const { t } = useTranslation('translation', { keyPrefix: 'timeline' });
const { qid = '', aid = '', tid = '' } = useParams();
const { is_admin } = loggedUserInfoStore((state) => state.user); const { is_admin } = loggedUserInfoStore((state) => state.user);
const [showVotes, setShowVotes] = useState(false);
const { data: timelineData } = useTimelineData({ const { data: timelineData } = useTimelineData({
object_id: '10010000000000001', object_id: tid || aid || qid,
object_type: 'question', show_vote: showVotes,
show_vote: false,
}); });
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 ( return (
<Container className="py-3"> <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"> <Row className="py-3 justify-content-center">
<Col xxl={10}> <Col xxl={10}>
<h5 className="mb-4"> <h5 className="mb-4">
{t('title')} <Link to="/">{timelineData?.object_info?.title}</Link> {t('title')}{' '}
<Link to={linkUrl}>{timelineData?.object_info?.title}</Link>
</h5> </h5>
<Form.Check {timelineData?.object_info.object_type !== 'tag' && (
className="mb-4" <Form.Check
type="switch" className="mb-4"
id="custom-switch" type="switch"
label={t('show_votes')} id="custom-switch"
/> label={t('show_votes')}
checked={showVotes}
onChange={(e) => handleSwitch(e.target.checked)}
/>
)}
<Table hover> <Table hover>
<thead> <thead>
<tr> <tr>
@ -123,9 +80,9 @@ const Index: FC = () => {
<HistoryItem <HistoryItem
data={item} data={item}
objectInfo={timelineData?.object_info} objectInfo={timelineData?.object_info}
key={item.revision_id} key={item.activity_id}
isAdmin={is_admin} 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', page: 'pages/Timeline',
guard: async () => { guard: async () => {
return guard.logged(); return guard.logged();

View File

@ -17,3 +17,14 @@ export const useTimelineData = (params: Type.TimelineReq) => {
mutate, 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(/<iframe/gi, '&lt;iframe')
?.replace(/<input/gi, '&lt;input'); ?.replace(/<input/gi, '&lt;input');
} }
const diff = Diff.diffChars(newText, oldText); const diff = Diff.diffChars(oldText, newText);
const result = diff.map((part) => { const result = diff.map((part) => {
if (part.added) { if (part.added) {
return `<span class="review-text-add">${part.value}</span>`; return `<span class="review-text-add">${part.value}</span>`;