Метод Onclick в React воздействует на каждый элемент списка в таблице, должен влиять только на тот, на который нажали - PullRequest
0 голосов
/ 09 апреля 2020

У меня есть файл 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>];
}
...