Repository: inovex/scrumlr.io
Pull request #1934 (2022-07-10 15:45:42)

Add Print/PDF Export View
## Changelog

- [X] Added compact print view that can be used for PDF exports
- [X] "Show Authors of Notes" propagates to the print view
- [X] Print dialog opens automatically from the start and can be reopened manually via a floating button

Note: Some background colors are missing if "Show backgrounds" is disabled in the print dialog (it was enabled from start in Chrome but not in Firefox).

## ToDo
- [x] Fix tests
- [x] Check note order/sorting without votes
- [x] Rotes Herz
- [x] Rechtsbündiges Datum + Line Height (Participants und Datum mehr Abstand)
- [x] Rahmen um Stacks
- [x] A4 Format, keine Notes abschneiden

## Off Topic
- [x] Fix JSON and CSV export filenames

## Checklist
- [x] I have performed a self-review of my own code
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] The light- and dark-theme are both supported and tested
- [x] The design was implemented and is responsive for all devices and screen sizes
- [x] The application was tested in the most commonly used browsers (e.g. Chrome, Firefox, Safari)

timengel: ![image](https://user-images.githubusercontent.com/32651718/178152071-77670ce1-0096-45d5-82f6-02dbb6b6d819.png)


Patch:
diff --git a/package.json b/package.json
index 97ebd97b..aa78d465 100644
--- a/package.json
+++ b/package.json
@@ -31,6 +31,7 @@
     "react-redux": "^7.2.8",
     "react-router": "^6.0.1",
     "react-router-dom": "^6.3.0",
+    "react-to-print": "^2.14.7",
     "react-toastify": "^9.0.5",
     "redux": "^4.2.0",
     "redux-thunk": "^2.4.1",
diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json
index 96fd8abc..ae38df9a 100644
--- a/public/locales/de/translation.json
+++ b/public/locales/de/translation.json
@@ -70,7 +70,8 @@
     "title": "Sitzung exportieren",
     "button": "Sitzung exportieren",
     "exportAsJson": "JSON exportieren",
-    "exportAsCSV": "CSV exportieren"
+    "exportAsCSV": "CSV exportieren",
+    "openPrintView": "Druckansicht öffnen"
   },
   "ShareQrCodeOption": {
     "title": "Sitzung teilen",
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 5cf19f3b..22d8e2de 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -70,7 +70,8 @@
     "title": "Export board",
     "button": "Export board",
     "exportAsJson": "Export as JSON",
-    "exportAsCSV": "Export as CSV"
+    "exportAsCSV": "Export as CSV",
+    "openPrintView": "Open print view"
   },
   "ShareQrCodeOption": {
     "title": "Share Session",
diff --git a/src/assets/icon-print.svg b/src/assets/icon-print.svg
new file mode 100644
index 00000000..4d5e81d4
--- /dev/null
+++ b/src/assets/icon-print.svg
@@ -0,0 +1,5 @@
+<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
+  <path fill="var(--ci-primary-color, currentColor)" d="M420,128.1V16H92V128.1A80.1,80.1,0,0,0,16,208V400H84V368H48V208a48.054,48.054,0,0,1,48-48H416a48.054,48.054,0,0,1,48,48V368H420v32h76V208A80.1,80.1,0,0,0,420,128.1Zm-32-.1H124V48H388Z" class="ci-primary"/>
+  <rect width="32" height="32" x="396" y="200" fill="var(--ci-primary-color, currentColor)" class="ci-primary"/>
+  <path fill="var(--ci-primary-color, currentColor)" d="M116,264H76v32h40V496H388V296h40V264H116ZM356,464H148V296H356Z" class="ci-primary"/>
+</svg>
diff --git a/src/components/SettingsDialog/ExportBoard/ExportBoard.tsx b/src/components/SettingsDialog/ExportBoard/ExportBoard.tsx
index acd8f053..986d6485 100644
--- a/src/components/SettingsDialog/ExportBoard/ExportBoard.tsx
+++ b/src/components/SettingsDialog/ExportBoard/ExportBoard.tsx
@@ -3,6 +3,8 @@ import {VFC} from "react";
 import {useTranslation} from "react-i18next";
 import {ReactComponent as ExportCSV} from "assets/icon-export-csv.svg";
 import {ReactComponent as ExportJSON} from "assets/icon-export-json.svg";
+import {ReactComponent as PrintIcon} from "assets/icon-print.svg";
+import {useNavigate} from "react-router";
 import {useAppSelector} from "../../../store";
 import {exportAsJSON, exportAsCSV} from "../../../utils/export";
 import {SettingsButton} from "../Components/SettingsButton";
@@ -11,8 +13,10 @@ import "../SettingsDialog.scss";
 
 export const ExportBoard: VFC = () => {
   const {t} = useTranslation();
+  const navigate = useNavigate();
 
   const boardId = useAppSelector((state) => state.board.data!.id);
+  const boardName = useAppSelector((state) => state.board.data!.name);
 
   return (
     <div data-testid="export" className="settings-dialog__container">
@@ -26,7 +30,7 @@ export const ExportBoard: VFC = () => {
           icon={ExportJSON}
           className="export-board__button-reverse-order"
           onClick={() => {
-            exportAsJSON(boardId);
+            exportAsJSON(boardId, boardName);
           }}
           data-testid="export-json"
         />
@@ -36,10 +40,19 @@ export const ExportBoard: VFC = () => {
           icon={ExportCSV}
           className="export-board__button-reverse-order"
           onClick={() => {
-            exportAsCSV(boardId);
+            exportAsCSV(boardId, boardName);
           }}
           data-testid="export-csv"
         />
+        <hr className="settings-dialog__separator" />
+        <SettingsButton
+          label={t("ExportBoardOption.openPrintView")}
+          icon={PrintIcon}
+          className="export-board__button-reverse-order"
+          onClick={() => {
+            navigate(`/board/${boardId}/print`);
+          }}
+        />
       </div>
     </div>
   );
diff --git a/src/components/SettingsDialog/ExportBoard/PrintView/PrintView.scss b/src/components/SettingsDialog/ExportBoard/PrintView/PrintView.scss
new file mode 100644
index 00000000..65f18840
--- /dev/null
+++ b/src/components/SettingsDialog/ExportBoard/PrintView/PrintView.scss
@@ -0,0 +1,239 @@
+@import "src/constants/style.scss";
+
+$note__border-radius: 16px;
+$border-radius--round: 50px;
+
+$color-backlog-blue-transparent: rgba(0, 87, 255, 0.25);
+$color-backlog-blue-background: rgba(0, 87, 255, 0.05);
+$color-button: $color-lean-lilac;
+$color-button-shadow: rgba(133, 144, 147, 0.5);
+
+@page {
+  margin: 11mm 17mm 17mm 17mm;
+}
+
+@media print {
+  .print-view__button-container {
+    display: none;
+  }
+  .print-view__note {
+    break-inside: avoid-page;
+  }
+}
+
+.print-view__container {
+  height: auto;
+  overflow: visible;
+}
+
+.print-view__button-container {
+  position: fixed;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  top: 92%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.print-view__button {
+  width: 35px;
+  height: 35px;
+  background: lighten($color-button, 35%);
+  border: none;
+  border-radius: $border-radius--round;
+  margin: $margin--small;
+  cursor: pointer;
+  font-weight: bold;
+  font-size: $text-size--medium;
+  box-shadow: 0 4px 7px 1px $color-button-shadow;
+  svg {
+    color: $color-button;
+  }
+
+  &:hover {
+    background: $color-button;
+    box-shadow: 0 4px 7px 1px darken($color-button-shadow, 15%);
+    svg {
+      color: $color-white;
+    }
+  }
+  &:active {
+    background: darken($color-button, 10%);
+    box-shadow: 0 4px 5px 1px darken($color-button-shadow, 25%);
+    svg {
+      color: $color-white;
+    }
+  }
+}
+
+.print-view__icon-print {
+  display: block;
+  margin: auto;
+  width: 18px;
+  height: 18px;
+}
+
+.print-view__icon-close {
+  display: block;
+  margin: auto;
+  width: 25px;
+  height: 25px;
+}
+
+.print-view {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  background-color: $color-white;
+  overflow: visible;
+  width: 850px;
+  padding: 40px 70px;
+}
+
+.print-view__title-wrapper {
+  width: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: $margin--large;
+}
+
+.print-view__title-text {
+  color: $color-black;
+  font-size: 32px;
+  font-weight: bold;
+  letter-spacing: $letter-spacing--large;
+  line-height: $line-height--large;
+  margin: 0;
+}
+
+.print-view__title-info {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  gap: $margin--small;
+  font-size: $text-size--small;
+  p {
+    margin: 0;
+  }
+}
+
+.print-view__column-list {
+  display: block;
+  width: 100%;
+  gap: $margin--default;
+}
+
+.print-view__column {
+  display: block;
+  width: 100%;
+  background-color: $color-white;
+  color: $color-black;
+}
+
+.print-view__column-header-wrapper {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  width: 100%;
+  padding: 0;
+  margin: $margin--small 0 $margin--default 0;
+}
+
+.print-view__column-header-text {
+  font-size: $text-size--large;
+  margin: 0 0 0 $margin--small;
+  font-weight: bold;
+  letter-spacing: $letter-spacing--large;
+  line-height: $line-height--large;
+  color: $color-black;
+  border-bottom: solid 3px $color-backlog-blue;
+}
+
+.print-view__column-header-card-count {
+  margin: 0 $margin--small 0 0;
+  color: $color-middle-gray;
+  font-weight: bold;
+}
+
+.print-view__note-stack-wrapper {
+  border-radius: $note__border-radius;
+  padding: $padding--small $padding--small 0 $padding--small;
+  border: 2px solid $color-backlog-blue-transparent;
+  width: 100%;
+  margin-bottom: $margin--default;
+  background: $color-backlog-blue-background;
+}
+
+.print-view__note {
+  display: flex;
+  flex-direction: column;
+  width: 100%;
+  margin-bottom: $margin--default;
+  padding: calc(#{$padding--small} + 4px);
+  border: 2px solid $color-backlog-blue-transparent;
+  border-radius: $note__border-radius;
+  background: $color-white;
+}
+
+.print-view__note--isChild {
+  width: 100%;
+  margin-bottom: $margin--small;
+  border: 2px dashed $color-backlog-blue-transparent;
+}
+
+.print-view__note--isTop {
+  margin-bottom: $margin--small;
+}
+
+.print-view__note-text {
+  margin: 0 0 $margin--medium 0;
+}
+
+.print-view__note-info-wrapper {
+  display: block;
+  position: relative;
+  height: $line-height--large;
+}
+
+.print-view__note-info--isTop {
+  svg {
+    color: $color-backlog-blue;
+  }
+}
+
+.print-view__note-info--isChild {
+  svg {
+    color: $color-backlog-blue-transparent;
+  }
+}
+
+.print-view__note-info-author {
+  position: absolute;
+  top: 50%;
+  transform: translateY(-50%);
+  display: inline;
+  width: auto;
+  margin-top: auto;
+  color: $color-middle-gray;
+  font-weight: bold;
+}
+
+.print-view__note-info-votes {
+  display: inline !important;
+  float: right;
+  border-radius: $border-radius--round;
+  padding: 6px 11px;
+  background: $color-backlog-blue-transparent;
+  color: $color-backlog-blue;
+  font-weight: bold;
+}
+
+.print-view__footer-container {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  width: 100%;
+  font-size: $text-size--small;
+}
diff --git a/src/components/SettingsDialog/ExportBoard/PrintView/PrintView.tsx b/src/components/SettingsDialog/ExportBoard/PrintView/PrintView.tsx
new file mode 100644
index 00000000..5b2234de
--- /dev/null
+++ b/src/components/SettingsDialog/ExportBoard/PrintView/PrintView.tsx
@@ -0,0 +1,203 @@
+import {ReactElement, useEffect, useRef, useState} from "react";
+import {API} from "api";
+import {useNavigate} from "react-router";
+import "./PrintView.scss";
+import {useReactToPrint} from "react-to-print";
+import {ReactComponent as ScrumlrLogo} from "assets/scrumlr-logo-light.svg";
+import {ReactComponent as PrintIcon} from "assets/icon-print.svg";
+import {ReactComponent as CloseIcon} from "assets/icon-close.svg";
+import {useTranslation} from "react-i18next";
+import classNames from "classnames";
+
+interface BoardData {
+  board: {
+    id: string;
+    name: string;
+    showAuthors: boolean;
+    showVoting: boolean;
+  };
+  columns: [
+    {
+      id: string;
+      name: string;
+    }
+  ];
+  notes: [
+    {
+      id: string;
+      author: string;
+      text: string;
+      votes: number;
+      position: {
+        column: string;
+        stack: string;
+        rank: number;
+      };
+    }
+  ];
+  participants: [
+    {
+      role: string;
+      user: {
+        id: string;
+        name: string;
+      };
+    }
+  ];
+  votings: [
+    {
+      votes: {
+        votes: number;
+        votesPerNote: [];
+      };
+    }
+  ];
+}
+
+interface PrintViewProps {
+  boardId: string;
+  boardName: string;
+}
+
+export const PrintView = ({boardId, boardName}: PrintViewProps) => {
+  const {t} = useTranslation();
+  const navigate = useNavigate();
+
+  const [boardData, setBoardData] = useState<BoardData>();
+
+  const pageStyle = `
+      @page {
+        size: A4;
+
+      }
+    `;
+
+  const printRef = useRef(null);
+  const handlePrint = useReactToPrint({
+    content: () => printRef.current,
+    documentTitle: boardName ?? "scrumlr.io",
+    pageStyle,
+  });
+
+  const getBoardData = async () => {
+    const response = await API.exportBoard(boardId, "application/json");
+    return response.json();
+  };
+
+  useEffect(() => {
+    if (!boardData) {
+      getBoardData()
+        .then((data) => setBoardData(data))
+        .then(handlePrint);
+    }
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, []);
+
+  const currDate = new Date();
+  const currDateStr = `${String(currDate.getDate()).padStart(2, "0")}.${String(currDate.getMonth() + 1).padStart(2, "0")}.${currDate.getFullYear()}, ${String(
+    currDate.getHours()
+  ).padStart(2, "0")}:${String(currDate.getMinutes()).padStart(2, "0")}`;
+
+  const getAuthorName = (authorId: string) => (boardData?.board.showAuthors ? boardData?.participants.filter((p) => p.user?.id === authorId)[0].user?.name : "");
+
+  const getNoteVotes = (noteId: string) => (boardData?.votings ? boardData?.votings[0].votes?.votesPerNote[noteId]?.total ?? 0 : 0);
+
+  const voteLabel = (noteId: string) => {
+    if (!(boardData?.votings && boardData?.board.showVoting)) return "";
+    const votes = getNoteVotes(noteId);
+    return votes > 0 ? <div className="print-view__note-info-votes">{votes} Votes</div> : "";
+  };
+
+  const compareNotes = (a: {id: string; position: {rank: number}}, b: {id: string; position: {rank: number}}) => {
+    if (boardData?.votings && getNoteVotes(a.id) !== getNoteVotes(b.id)) {
+      return getNoteVotes(a.id) > getNoteVotes(b.id) ? -1 : 1;
+    }
+    return a.position.rank > b.position.rank ? -1 : 1;
+  };
+
+  const getChildNotes = (noteId: string) => boardData?.notes.filter((n) => n.position.stack === noteId).sort((a, b) => compareNotes(a, b));
+
+  const noteElement = (id: string, text: string, authorId: string, isChild: boolean, isTop: boolean) => (
+    <div key={id} className={classNames("print-view__note", {"print-view__note--isChild": isChild, "print-view__note--isTop": isTop})}>
+      <p className="print-view__note-text">{text}</p>
+      <div className="print-view__note-info-wrapper">
+        <span className="print-view__note-info-author">{getAuthorName(authorId)}</span>
+        <span className={classNames({"print-view__note-info--isTop": isTop, "print-view__note-info--isChild": isChild})}>{voteLabel(id)}</span>
+      </div>
+    </div>
+  );
+
+  const noteStackWrapper = (input: ReactElement, childNotes: {id: string; text: string; author: string}[]) => (
+    <div className="print-view__note-stack-wrapper">
+      {input}
+      <div className="print-view__note-stack-child-wrapper">{childNotes.map((n) => noteElement(n.id, n.text, n.author, true, false))}</div>
+    </div>
+  );
+
+  const columnHasNotes = (columnId: string) => columnId && !!boardData?.notes.filter((n) => n.position.column === columnId).length;
+
+  const handleClose = () => navigate(`/board/${boardId}`);
+
+  return (
+    <div className="print-view__container">
+      <div className="print-view__button-container">
+        <button className="print-view__button" onClick={handlePrint}>
+          <PrintIcon className="print-view__icon-print" />
+        </button>
+        <button className="print-view__button" onClick={handleClose}>
+          <CloseIcon className="print-view__icon-close" />
+        </button>
+      </div>
+      <div ref={printRef} className="print-view">
+        <div className="print-view__title-wrapper">
+          <ScrumlrLogo />
+          <h1 className="print-view__title-text">{boardData?.board.name ?? "Scrumlr.io"}</h1>
+          <div className="print-view__title-info">
+            <p>{currDateStr}</p>
+            <p>{`${boardData?.participants.length} ${t("SettingsDialog.Participants")}`}</p>
+          </div>
+        </div>
+        <div className="print-view__column-list">
+          {boardData &&
+            boardData.columns?.map(
+              (c) =>
+                columnHasNotes(c.id) && (
+                  <div key={c.id} className="print-view__column">
+                    <div className="print-view__column-header-wrapper">
+                      <h2 className="print-view__column-header-text">{c.name}</h2>
+                      <div className="print-view__column-header-card-count">{boardData.notes.filter((n) => n.position.column === c.id).length} notes</div>
+                    </div>
+                    {boardData.notes
+                      .filter((n) => n.position.column === c.id)
+                      .sort((a, b) => compareNotes(a, b))
+                      .map((n) => {
+                        if (!n.position.stack) {
+                          const childNotes = getChildNotes(n.id);
+                          return childNotes && childNotes.length > 0
+                            ? noteStackWrapper(noteElement(n.id, n.text, n.author, false, true), childNotes)
+                            : noteElement(n.id, n.text, n.author, false, false);
+                        }
+                        return "";
+                      })}
+                  </div>
+                )
+            )}
+        </div>
+        <div className="print-view__footer-container">
+          <p>
+            Generated on{" "}
+            <a href="https://scrumlr.io/" target="_blank" rel="noopener noreferrer">
+              scrumlr.io
+            </a>
+          </p>
+          <p>
+            Provided with ♥️ by{" "}
+            <a href="https://www.inovex.de/" target="_blank" rel="noopener noreferrer">
+              inovex
+            </a>
+          </p>
+        </div>
+      </div>
+    </div>
+  );
+};
diff --git a/src/components/SettingsDialog/ExportBoard/PrintView/index.ts b/src/components/SettingsDialog/ExportBoard/PrintView/index.ts
new file mode 100644
index 00000000..3a0ba417
--- /dev/null
+++ b/src/components/SettingsDialog/ExportBoard/PrintView/index.ts
@@ -0,0 +1,1 @@
+export * from "./PrintView";
diff --git a/src/routes/Board/BoardGuard.tsx b/src/routes/Board/BoardGuard.tsx
index e496fa85..29084a3a 100644
--- a/src/routes/Board/BoardGuard.tsx
+++ b/src/routes/Board/BoardGuard.tsx
@@ -7,13 +7,19 @@ import {PassphraseDialog} from "components/PassphraseDialog";
 import {useParams} from "react-router";
 import {useTranslation} from "react-i18next";
 import {Button} from "components/Button";
+import {PrintView} from "components/SettingsDialog/ExportBoard/PrintView";
 import {Board} from "./Board";
 
-export const BoardGuard = () => {
+interface BoardGuardProps {
+  printViewEnabled: boolean;
+}
+
+export const BoardGuard = ({printViewEnabled}: BoardGuardProps) => {
   const {boardId} = useParams<"boardId">();
   const {t} = useTranslation();
 
   const boardStatus = useAppSelector((state) => state.board.status);
+  const boardName = useAppSelector((applicationState) => applicationState.board.data?.name);
 
   useEffect(() => {
     store.dispatch(Actions.joinBoard(boardId!));
@@ -23,6 +29,10 @@ export const BoardGuard = () => {
     };
   }, [boardId]);
 
+  if (printViewEnabled && boardId) {
+    return <PrintView boardId={boardId} boardName={boardName ?? "scrumlr.io"} />;
+  }
+
   if (boardStatus === "accepted" || boardStatus === "ready") {
     return <Board />;
   }
diff --git a/src/routes/Router.tsx b/src/routes/Router.tsx
index a118e1fc..9c346957 100644
--- a/src/routes/Router.tsx
+++ b/src/routes/Router.tsx
@@ -37,11 +37,19 @@ const Router = () => (
         }
       />
       <Route path="/login" element={<LoginBoard />} />
+      <Route
+        path="/board/:boardId/print"
+        element={
+          <RequireAuthentication>
+            <BoardGuard printViewEnabled />
+          </RequireAuthentication>
+        }
+      />
       <Route
         path="/board/:boardId"
         element={
           <RequireAuthentication>
-            <BoardGuard />
+            <BoardGuard printViewEnabled={false} />
           </RequireAuthentication>
         }
       >
diff --git a/src/utils/export.ts b/src/utils/export.ts
index 835d2197..b0fc6bb3 100644
--- a/src/utils/export.ts
+++ b/src/utils/export.ts
@@ -9,7 +9,7 @@ export const fileName = (name?: string) => {
 export const exportAsCSV = async (id: string, name?: string) => {
   const response = await API.exportBoard(id, "text/csv");
   const blob = await response.blob();
-  saveAs(blob, `${fileName(name)}.csv`);
+  saveAs(blob, `${fileName(name ?? "scrumlr.io")}.csv`);
 };
 
 export const exportAsJSON = async (id: string, name?: string) => {
diff --git a/yarn.lock b/yarn.lock
index 05104584..9b892dd8 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8413,6 +8413,13 @@ react-side-effect@^2.1.0:
   resolved "https://registry.yarnpkg.com/react-side-effect/-/react-side-effect-2.1.1.tgz#66c5701c3e7560ab4822a4ee2742dee215d72eb3"
   integrity sha512-2FoTQzRNTncBVtnzxFOk2mCpcfxQpenBMbk5kSVBg5UcPqV9fRbgY2zhb7GTWWOlpFmAxhClBDlIq8Rsubz1yQ==
 
+react-to-print@^2.14.7:
+  version "2.14.7"
+  resolved "https://registry.yarnpkg.com/react-to-print/-/react-to-print-2.14.7.tgz#d288de0e573e6e63e203a48d4bad7a1759d18688"
+  integrity sha512-lWVVAs9Co25uyE0toxcWeFsmaZObwUozXrJD9WMpDPclpBgk+WIzxlt3Q3omL/BCBG/cpf0XNvhayUWa+99YGw==
+  dependencies:
+    prop-types "^15.8.1"
+
 react-toastify@^9.0.5:
   version "9.0.5"
   resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.0.5.tgz#310d7bcfcf3887c0e8461ac6068fe1abb8d720e1"


Test patch:
diff --git a/src/components/SettingsDialog/ExportBoard/__tests__/ExportBoard.test.tsx b/src/components/SettingsDialog/ExportBoard/__tests__/ExportBoard.test.tsx
index edb8dd6f..5deb6986 100644
--- a/src/components/SettingsDialog/ExportBoard/__tests__/ExportBoard.test.tsx
+++ b/src/components/SettingsDialog/ExportBoard/__tests__/ExportBoard.test.tsx
@@ -5,6 +5,7 @@ import {ExportBoard} from "../ExportBoard";
 import {I18nextProvider} from "react-i18next";
 import i18nTest from "i18nTest";
 import getTestStore from "utils/test/getTestStore";
+import {BrowserRouter} from "react-router-dom";
 
 jest.mock("utils/export", () => ({
   ...jest.requireActual("utils/export"),
@@ -17,7 +18,9 @@ const renderExportBoard = () => {
   return render(
     <I18nextProvider i18n={i18nTest}>
       <Provider store={getTestStore()}>
-        <ExportBoard />
+        <BrowserRouter>
+          <ExportBoard />
+        </BrowserRouter>
       </Provider>
     </I18nextProvider>
   );
diff --git a/src/components/SettingsDialog/ExportBoard/__tests__/__snapshots__/ExportBoard.test.tsx.snap b/src/components/SettingsDialog/ExportBoard/__tests__/__snapshots__/ExportBoard.test.tsx.snap
index 672b2fc0..b8cbc53d 100644
--- a/src/components/SettingsDialog/ExportBoard/__tests__/__snapshots__/ExportBoard.test.tsx.snap
+++ b/src/components/SettingsDialog/ExportBoard/__tests__/__snapshots__/ExportBoard.test.tsx.snap
@@ -57,6 +57,24 @@ Object {
               icon-export-csv.svg
             </svg>
           </button>
+          <hr
+            class="settings-dialog__separator"
+          />
+          <button
+            class="settings-option-button export-board__button-reverse-order"
+            type="button"
+          >
+            <span
+              class="settings-option-button__label"
+            >
+              Open print view
+            </span>
+            <svg
+              class="settings-option-button__icon"
+            >
+              icon-print.svg
+            </svg>
+          </button>
         </div>
       </div>
     </div>
@@ -114,6 +132,24 @@ Object {
             icon-export-csv.svg
           </svg>
         </button>
+        <hr
+          class="settings-dialog__separator"
+        />
+        <button
+          class="settings-option-button export-board__button-reverse-order"
+          type="button"
+        >
+          <span
+            class="settings-option-button__label"
+          >
+            Open print view
+          </span>
+          <svg
+            class="settings-option-button__icon"
+          >
+            icon-print.svg
+          </svg>
+        </button>
       </div>
     </div>
   </div>,