У меня есть файл TSX, который создает таблицу комментариев. Когда страница отображается, собирается массив информации, представляющей комментарии. Набор, содержащий индексы комментариев в массиве, который является частью состояния, определяет, будет ли ссылка под комментарием читать «показать больше» или «показать меньше». Как только эта ссылка нажата, индекс комментария добавляется в набор, а состояние обновляется с добавлением этого индекса в набор. Если набор содержит индекс комментария при обновлении состояния, он должен прочитать 'показывай меньше'. Проблема в том, что когда я нажимаю эту ссылку, она меняет ссылку на каждый элемент списка в таблице, а не на один.
import * as React from "react";
import { LocalizationInfo } from "emt-localization";
import { DateHandler } from "../handlers/date-handler";
import { UserIcon } from "./user-icon";
import { OnDownloadDocument } from "../models/generic-types";
import { getUserString } from "../models/user";
import { ClaimAction, ActionDetails, ReasonActionDetails, CommentDetails, DocumentDetails } from "../models/action";
const stateChangeActions = new Set<string>([
'Dispute',
'Rejection',
'SetUnderReview',
'Approval',
'Recall',
'Submission',
'RequestPartnerAction'
]);
const fiveMinutes = 5 * 60 * 1000;
const underscoreRegex = /_[^_]*_/g;
const actionTypeClassMap: {[actionType: string]: string} = {
'Submission': 'success',
'RequestPartnerAction': 'warn',
'Rejection': 'warn',
'Approval': 'success',
'Recall': 'warn',
'Dispute': 'warn',
'SetUnderReview': 'info',
'CustomerConsentDeclined': 'rejected'
};
const maxShortLength = 100;
interface ActionsProps {
readonly localization: LocalizationInfo;
readonly actions: ClaimAction[];
readonly onDownloadDocument: OnDownloadDocument;
readonly isInternalFacing: boolean;
readonly isHistoryPaneDisplay: boolean;
}
class ActionsState {
readonly expandedComments = new Set<number>();
}
export class Actions extends React.Component<ActionsProps, ActionsState> {
constructor(props: ActionsProps) {
super(props);
this.state = new ActionsState();
}
render():JSX.Element {
const loc = this.props.localization;
const isInternalFacing = this.props.isInternalFacing;
const toTime = (action:ClaimAction) => new Date(action.timeStamp).getTime();
const sortBy = (a:ClaimAction, b:ClaimAction) => toTime(b) - toTime(a);
const actions = this.props.actions.filter(action => {
if (isDocumentDetails(action.details)) {
if (action.actionType == 'DocumentSubmission' && action.details.documentType == 'Invoice') {
return false;
}
}
return true;
});
actions.sort(sortBy);
const grouped = groupActions(actions);
return (
<ul className={`claim-actions ${this.props.isHistoryPaneDisplay ? '' : 'user-comment-box'}`}>
{grouped.map((actions, index) => {
let actionClass = '';
actions.forEach(action => {
actionClass = actionClass || actionTypeClassMap[action.actionType];
});
const first = actions[0];
const icon = actionClass == 'success' ?
sequenceIcon('complete') :
actionClass == 'warn' ?
sequenceIcon('action-required') :
actionClass == 'rejected' ?
sequenceIcon('rejected') :
actionClass == 'info' ?
sequenceIcon('editing') :
<UserIcon
user={first.user}
isInternalFacing={isInternalFacing}
/>;
const elements = actions.map((action, actionIndex) => this.renderAction(action, actionIndex));
return (
<li className={actionClass} key={index}>
{icon}
<div className="win-color-fg-secondary">
<span className="claim-action-name win-color-fg-primary">
{ getUserString(first.user, isInternalFacing) }
</span>
<span className="text-caption">
{loc.piece("HistoryItemTitle", 0)}
{DateHandler.friendlyDate(first.timeStamp, true)}
</span>
</div>
{elements}
</li>
)
})}
</ul>
)
}
private renderAction(action:ClaimAction, actionIndex:number):JSX.Element|null {
const strings = this.props.localization.strings;
if (action.actionType == 'AddComments' || action.actionType == 'AddInternalComments') {
return this.renderComment((action.details as CommentDetails).comments, actionIndex);
}
const document = isDocumentDetails(action.details) ? action.details : null;
const documentLink = document ?
(key:number) =>
<a
key={key}
onClick={() => this.props.onDownloadDocument(document.documentId, document.name)}>
{document.name}
</a>
: null;
const locKey = `History_${action.actionType}`;
const localizedFlat = strings[locKey] || "";
const localized = replaceUnderscores(localizedFlat, documentLink);
const reason = (action.actionType === 'RequestPartnerAction' || action.actionType === 'Rejection') && action.details ? (action.details as ReasonActionDetails).reasonCode : '';
const reasonString = reason.charAt(0).toUpperCase() + reason.slice(1)
if (localized) {
return (
<div key={actionIndex}>
<div className="claim-action">
<span className="text-caption">{localized}</span>
</div>
<div className="claim-action">
{ reasonString && <span className="text-caption"><strong>{strings['ReasonLabel']}</strong>{` ${strings[reasonString]}`}</span> }
</div>
</div>
);
}
console.error(`Unknown action type ${action.actionType}`);
return null;
}
private renderComment(comment: string, actionIndex: number): JSX.Element {
const strings = this.props.localization.strings;
const canShorten = comment.length > maxShortLength;
const shouldShorten = canShorten && !this.state.expandedComments.has(actionIndex);
const shortened = shouldShorten ?
comment.substring(0, maxShortLength) + "\u2026" :
comment;
const paragraphs = shortened
.split('\n')
.map(s => s.trim())
.filter(s => s);
const elements = paragraphs.map((comment, i) =>
<div className="claim-comment" key={i}>
{comment}
</div>
);
const toggle = () => {
const next = new Set<number>(this.state.expandedComments);
if (next.has(actionIndex)) {
next.delete(actionIndex)
}
else {
next.add(actionIndex);
}
this.setState({ expandedComments: next });
}
const makeLink = (locKey:string) =>
<a
onClick={toggle}>{strings[locKey]}</a>;
const afterLink = canShorten ?
shouldShorten ?
makeLink('ShowMore') :
makeLink('ShowLess') :
null;
return (
<React.Fragment key={actionIndex}>
{elements}
{afterLink}
</React.Fragment>
);
}
}
// Function groups actions together under some conditions
function groupActions(actions:ClaimAction[]):ClaimAction[][] {
const grouped:ClaimAction[][] = [];
actions.forEach(action => {
if (grouped.length) {
const lastGroup = grouped[grouped.length - 1];
const timeDifference = new Date(lastGroup[0].timeStamp).getTime() - new Date(action.timeStamp).getTime();
if (stateChangeActions.has(lastGroup[0].actionType) && action.actionType == 'AddComments' && timeDifference < fiveMinutes) {
lastGroup.push(action);
return;
}
}
grouped.push([action]);
});
return grouped;
}
function isDocumentDetails(details:ActionDetails|null): details is DocumentDetails {
return !!details && (details.$concreteClass == 'InvoiceActionDetails' || details.$concreteClass == 'DocumentActionDetails');
}
function sequenceIcon(className: string):JSX.Element {
return (
<div className="sequence sequence-status claims-icon">
<div className={`step ${className}`} />
</div>
);
}
function replaceUnderscores(str: string, documentLink: ((k:number)=>JSX.Element)|null, startKey: number=0):JSX.Element[] {
if (!str) {
return [];
}
const match = underscoreRegex.exec(str);
if (!match) {
return replaceDocumentLink(str, documentLink, startKey);
}
const firstText = str.substring(0, match.index);
const middleText = match[0].substring(1, match[0].length - 1);
const lastText = str.substring(match.index + match[0].length);
const first = replaceUnderscores(firstText, documentLink, startKey);
const middle = [<strong className={'claims-emphasis'} key={startKey + first.length}>{middleText}</strong>];
const last = replaceUnderscores(lastText, documentLink, startKey + first.length + 1);
return first.concat(middle, last);
}
function replaceDocumentLink(str: string, documentLink: ((k:number)=>JSX.Element)|null, startKey: number=0):JSX.Element[] {
const replaceIndex = str.indexOf('{0}');
if (replaceIndex >= 0 && documentLink) {
return [
<React.Fragment key={startKey}>{str.substring(0, replaceIndex)}</React.Fragment>,
documentLink(startKey+1),
<React.Fragment key={startKey + 2}>{str.substring(replaceIndex+3)}</React.Fragment>
];
}
return [<React.Fragment key={startKey}>{str}</React.Fragment>];
}