All files / src/components WhatsNewModal.tsx

100% Statements 51/51
71.42% Branches 5/7
100% Functions 1/1
100% Lines 51/51

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 1221x   1x 1x 1x 1x 1x 1x                                               1x                                     1x 1x 34x   34x 34x   34x 34x 34x 34x 34x 34x   34x 34x 34x     34x 6x 6x     34x 34x 34x 34x 34x 34x     34x 34x 34x 34x                 34x 34x 34x 34x 34x   34x 23x 23x 23x   23x 23x       23x 34x 34x 34x             34x  
import { i18n } from '@/helpers/i18n';
import type { UpdateNotice } from '@/hooks/useUpdateAvailable';
import Button from '@mui/material/Button';
import Modal from '@mui/material/Modal';
import Stack from '@mui/material/Stack';
import Typography from '@mui/material/Typography';
import { AboutModalBox } from './StyledComponents';
import styles from './WhatsNewModal.module.scss';
 
/**
 * The subset of an update notice this modal renders. Accepts both a pending
 * update and an already-installed one; `source` is only present on the former.
 */
type WhatsNewNotice = Pick<UpdateNotice, 'version' | 'notes'> &
  Partial<Pick<UpdateNotice, 'source'>>;
 
/**
 * Props for {@link WhatsNewModal}.
 * - `notice` - The release whose notes are shown; `undefined` renders nothing.
 * - `open` - Whether the modal is visible.
 * - `onClose` - Invoked when the user closes without applying.
 * - `onApply` - Call to action for a pending update. Omit for notes about an
 *   already-installed release, which leaves a single acknowledging button.
 */
interface WhatsNewModalProps {
  notice: WhatsNewNotice | undefined;
  open: boolean;
  onClose: () => void;
  onApply?: () => void;
}
 
/**
 * Lists the release-note highlights for a pending update, grouped by the
 * headings from the release body (Added / Changed / Fixed …), with the call to
 * action for the user's install type. Opened from the update snackbar, which
 * stays compact because the popup has very little vertical room.
 *
 * The notes come from the GitHub release body, which the release workflow fills
 * from `CHANGELOG.md`. When they're unavailable — offline, rate-limited, or a
 * release published without a body — the modal says so rather than showing an
 * empty list, and the action still works.
 * @component
 * @category Components
 * @param props - The modal props (see {@link WhatsNewModalProps}).
 * @returns The rendered what's-new modal.
 * @example
 * ```tsx
 * <WhatsNewModal notice={notice} open={open} onClose={close} onApply={applyUpdate} />
 * ```
 * @source
 */
export function WhatsNewModal({ notice, open, onClose, onApply }: WhatsNewModalProps) {
  if (!notice) return null;
 
  const actionLabel =
    notice.source === 'webstore' ? i18n('update_action_reload') : i18n('update_action_view');
 
  return (
    <Modal
      data-testid="whats-new-modal"
      open={open}
      onClose={onClose}
      aria-labelledby="whats-new-title"
    >
      <AboutModalBox className={styles['whats-new-box']}>
        <Typography id="whats-new-title" variant="h6" component="h2" align="center" gutterBottom>
          {i18n('update_modal_title', [notice.version])}
        </Typography>
 
        {notice.notes.length === 0 ? (
          <Typography variant="body2" color="text.secondary" data-testid="whats-new-empty">
            {i18n('update_notes_unavailable')}
          </Typography>
        ) : (
          <Stack className={styles['whats-new-sections']} spacing={1.5}>
            {notice.notes.map((section, sectionIndex) => (
              <div key={section.title ?? `section-${sectionIndex}`}>
                {section.title && (
                  <Typography variant="overline" color="text.secondary" component="h3">
                    {section.title}
                  </Typography>
                )}
                <ul className={styles['whats-new-list']}>
                  {section.items.map((item) => (
                    <Typography key={item} variant="body2" component="li">
                      {item}
                    </Typography>
                  ))}
                </ul>
              </div>
            ))}
          </Stack>
        )}
 
        <Stack
          direction="row"
          spacing={1}
          justifyContent="flex-end"
          className={styles['whats-new-actions']}
        >
          {onApply ? (
            <>
              <Button data-testid="whats-new-close" onClick={onClose} color="inherit">
                {i18n('update_later')}
              </Button>
              <Button data-testid="whats-new-apply" onClick={onApply} variant="contained">
                {actionLabel}
              </Button>
            </>
          ) : (
            // Already installed — there's nothing to apply, so one button that
            // just acknowledges the notes.
            <Button data-testid="whats-new-close" onClick={onClose} variant="contained">
              {i18n('update_got_it')}
            </Button>
          )}
        </Stack>
      </AboutModalBox>
    </Modal>
  );
}