department-of-veterans-affairs/vets-website

View on GitHub
src/applications/mhv-secure-messaging/components/MessageList/MessageList.jsx

Summary

Maintainability
D
3 days
Test Coverage
/*
This component handles:
- displaying a list of 10 messages per page
- pagination logic
- sorting messages by sent date (desc - default, asc) and by sender's name (alpha desc or asc)

Assumptions that may need to be addressed:
- This component assumes it receives a payload containing ALL messages. Of the provided
pagination metadata, per_page and total_entries is used. If each page change requires another 
api call to fetch the next set of messages, this logic will need to be refactored, but shouldn't be difficult.
*/

import React, {
  useEffect,
  useState,
  useRef,
  useCallback,
  useMemo,
} from 'react';
import PropTypes from 'prop-types';
import { chunk } from 'lodash';
import { VaPagination } from '@department-of-veterans-affairs/component-library/dist/react-bindings';
import { focusElement } from '@department-of-veterans-affairs/platform-utilities/ui';
import { recordEvent } from '@department-of-veterans-affairs/platform-monitoring/exports';
import { useDispatch } from 'react-redux';
import { useLocation } from 'react-router-dom';
import MessageListItem from './MessageListItem';
import ThreadListSort from '../ThreadList/ThreadListSort';
import { setSearchPage, setSearchSort } from '../../actions/search';
import { Paths, threadSortingOptions } from '../../util/constants';

// Arbitrarily set because the VaPagination component has a required prop for this.
// This value dictates how many pages are displayed in a pagination component
const MAX_PAGE_LIST_LENGTH = 7;
const {
  SENT_DATE_ASCENDING,
  SENT_DATE_DESCENDING,
  DRAFT_DATE_ASCENDING,
  DRAFT_DATE_DESCENDING,
  RECEPIENT_ALPHA_ASCENDING,
  RECEPIENT_ALPHA_DESCENDING,
  SENDER_ALPHA_ASCENDING,
  SENDER_ALPHA_DESCENDING,
} = threadSortingOptions;
const MessageList = props => {
  const dispatch = useDispatch();
  const { folder, messages, keyword, isSearch, sortOrder, page } = props;

  const location = useLocation();
  const perPage = 10;
  const totalEntries = messages?.length;
  const multipleThreads = totalEntries > 1 ? 's' : '';
  const sortedListText =
    (location.pathname === Paths.DRAFTS ? 'draft' : 'message') +
    multipleThreads;

  const [currentMessages, setCurrentMessages] = useState([]);
  const [displayNums, setDisplayNums] = useState({
    from: 0,
    to: 0,
    label: '',
  }); // [from, to]
  const paginatedMessages = useRef([]);
  const displayingNumberOfMesssagesRef = useRef();

  // split messages into pages
  const paginateData = useCallback(
    data => {
      return chunk(data, perPage);
    },
    [perPage],
  );

  const fromToNums = useMemo(
    () => {
      const from = (page - 1) * perPage + 1;
      const to = Math.min(page * perPage, messages?.length);
      return { from, to };
    },
    [page, perPage, messages?.length],
  );

  // sort messages
  const sortMessages = useCallback(
    data => {
      return data.sort((a, b) => {
        if (
          [SENT_DATE_DESCENDING.value, DRAFT_DATE_DESCENDING.value].includes(
            sortOrder,
          ) ||
          (sortOrder === SENDER_ALPHA_ASCENDING.value &&
            a.senderName === b.senderName) ||
          (sortOrder === SENDER_ALPHA_DESCENDING.value &&
            a.senderName === b.senderName) ||
          (sortOrder === RECEPIENT_ALPHA_DESCENDING.value &&
            a.recipientName === b.recipientName)
        ) {
          return b.sentDate > a.sentDate ? 1 : -1;
        }
        if (
          [SENT_DATE_ASCENDING.value, DRAFT_DATE_ASCENDING.value].includes(
            sortOrder,
          )
        ) {
          return a.sentDate > b.sentDate ? 1 : -1;
        }

        if (sortOrder === SENDER_ALPHA_ASCENDING.value) {
          return a.senderName.toLowerCase() > b.senderName.toLowerCase()
            ? 1
            : -1;
        }
        if (sortOrder === SENDER_ALPHA_DESCENDING.value) {
          return a.senderName.toLowerCase() < b.senderName.toLowerCase()
            ? 1
            : -1;
        }
        if (sortOrder === RECEPIENT_ALPHA_ASCENDING.value) {
          return a.recipientName.toLowerCase() > b.recipientName.toLowerCase()
            ? 1
            : -1;
        }

        if (sortOrder === RECEPIENT_ALPHA_DESCENDING.value) {
          return a.recipientName.toLowerCase() < b.recipientName.toLowerCase()
            ? 1
            : -1;
        }
        return 0;
      });
    },
    [sortOrder],
  );

  // run once on component mount to set initial message and page data
  useEffect(
    () => {
      if (messages?.length) {
        paginatedMessages.current = paginateData(sortMessages(messages));

        setCurrentMessages(paginatedMessages.current[page - 1]);
      }
    },
    [page, messages, paginateData, sortMessages],
  );

  // update pagination values on...page change
  const onPageChange = pageValue => {
    setCurrentMessages(paginatedMessages.current[pageValue - 1]);
    dispatch(setSearchPage(pageValue));
    focusElement(displayingNumberOfMesssagesRef.current);
  };

  useEffect(
    () => {
      // get display numbers
      if (fromToNums && messages.length) {
        const label = `Showing ${fromToNums.from} - ${fromToNums.to} of ${
          messages.length
        } found messages`;
        setDisplayNums({ ...fromToNums, label });
      }
    },
    [fromToNums, messages.length],
  );

  const sortCallback = sortOrderValue => {
    dispatch(setSearchSort(sortOrderValue));
    dispatch(setSearchPage(1));
    recordEvent({
      event: 'cta-button-click',
      'button-type': 'primary',
      'button-click-label': 'Sort filtered messages',
    });
    focusElement(displayingNumberOfMesssagesRef.current);
  };

  return (
    <div className="message-list vads-l-row vads-u-flex-direction--column">
      {messages?.length > 1 && (
        <ThreadListSort sortOrder={sortOrder} sortCallback={sortCallback} />
      )}

      <h2 className="sr-only">List of filtered conversations</h2>
      <div
        role="status"
        ref={displayingNumberOfMesssagesRef}
        className="vads-u-padding-y--1 vads-l-row vads-u-margin-top--2 vads-u-border-top--1px vads-u-border-bottom--1px vads-u-border-color--gray-light"
      >
        {`Showing ${displayNums.from} to ${
          displayNums.to
        } of ${totalEntries} ${sortedListText}`}

        <span className="sr-only">
          {` sorted by ${threadSortingOptions[sortOrder].label}`}
        </span>
      </div>
      {currentMessages?.length > 0 &&
        currentMessages.map((message, idx) => (
          <MessageListItem
            key={`${message.messageId}+${idx}`}
            messageId={message.messageId}
            senderName={message.senderName}
            sentDate={message.sentDate}
            subject={message.subject}
            readReceipt={message.readReceipt}
            attachment={message.attachment}
            recipientName={message.recipientName}
            keyword={keyword}
            category={message.category}
            activeFolder={folder}
          />
        ))}
      {page === paginatedMessages.current.length && (
        <p className="vads-u-margin-y--3 vads-u-color--gray-medium">
          End of {!isSearch ? 'messages in this folder' : 'search results'}
        </p>
      )}
      {currentMessages &&
        paginatedMessages.current.length > 1 && (
          <VaPagination
            onPageSelect={e => onPageChange(e.detail.page)}
            page={page}
            pages={paginatedMessages.current.length}
            maxPageListLength={MAX_PAGE_LIST_LENGTH}
            showLastPage
          />
        )}
    </div>
  );
};

export default MessageList;

MessageList.propTypes = {
  folder: PropTypes.object,
  isSearch: PropTypes.bool,
  keyword: PropTypes.string,
  messages: PropTypes.array,
  page: PropTypes.number,
  sortOrder: PropTypes.string,
};