Repository: inovex/scrumlr.io
Pull request #2704 (2023-02-23 12:46:33)

Js/2595 reset ready state banner
## Description

Implements #2595: A banner in the participants list that allows moderators to reset the ready state of all users.

## Changelog

- Add a footer in the participants list
- It is only displayed to moderators and when at least one user exists that is ready
- Clicking on the `Reset`-Button resets the ready state of all users
- The footer then disappears with a little animation until there are ready users again

## 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)

## (Optional) Visual Changes
![Bildschirm­foto 2023-02-23 um 13 40 34](https://user-images.githubusercontent.com/7889564/220909339-4c439d99-a50e-4e71-897f-c32cd87cb165.png)




Patch:
diff --git a/public/locales/de/translation.json b/public/locales/de/translation.json
index 772de26a..428c023c 100644
--- a/public/locales/de/translation.json
+++ b/public/locales/de/translation.json
@@ -290,7 +290,9 @@
     "ParticipantFilterTooltip": "Zeige nur Teilnehmer",
     "OnlineFilterTooltip": "Zeige nicht verbundene Teilnehmer",
     "ChangeRoleToParticipantTooltip": "Ändere Rolle zu Teilnehmer",
-    "ChangeRoleToModeratorTooltip": "Ändere Rolle zu Moderator"
+    "ChangeRoleToModeratorTooltip": "Ändere Rolle zu Moderator",
+    "ResetBannerText": "Fertige Teilnehmer zurücksetzen.",
+    "ResetBannerButton": "Zurücksetzen"
   },
   "UserRole": {
     "Owner": "Besitzer",
diff --git a/public/locales/en/translation.json b/public/locales/en/translation.json
index 61f59055..8d6e61fe 100644
--- a/public/locales/en/translation.json
+++ b/public/locales/en/translation.json
@@ -289,7 +289,9 @@
     "ParticipantFilterTooltip": "Show only participants",
     "OnlineFilterTooltip": "Show disconnected participants",
     "ChangeRoleToParticipantTooltip": "Change role to participant",
-    "ChangeRoleToModeratorTooltip": "Change role to moderator"
+    "ChangeRoleToModeratorTooltip": "Change role to moderator",
+    "ResetBannerText": "Reset ready states of all users.",
+    "ResetBannerButton": "Reset"
   },
   "UserRole": {
     "Owner": "Owner",
diff --git a/src/components/SettingsDialog/Participants/Participants.scss b/src/components/SettingsDialog/Participants/Participants.scss
index 81ec62fa..1887d8e0 100644
--- a/src/components/SettingsDialog/Participants/Participants.scss
+++ b/src/components/SettingsDialog/Participants/Participants.scss
@@ -109,7 +109,6 @@
   list-style-type: none;
   padding: 0;
   overflow: scroll;
-  margin-bottom: $settings-dialog-container--bottom;
 }
 
 .participants__list-item {
@@ -188,6 +187,73 @@
   gap: $margin--small;
 }
 
+.participants-reset-state-banner__container {
+  margin-top: auto; // position at the very bottom
+  margin-left: -$settings-dialog-container--sides-small;
+  padding: $padding--small $padding--medium; // create some space
+
+  display: flex;
+  flex-direction: row;
+  justify-content: space-evenly;
+  align-items: center;
+
+  width: calc(100% + 2 * $settings-dialog-container--sides-small); // overflow over parent margin
+
+  background-color: $color-lighter-gray;
+
+  transform: translateY(100%);
+  transition: transform ease-in-out 0.3s;
+}
+
+.participants-reset-state-banner__container--is-active {
+  transform: translateY(0);
+}
+
+.participants-reset-state-banner__icon-and-text {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  column-gap: 4px;
+}
+
+.participants-reset-state-banner__check-icon {
+  width: $icon--large;
+  height: $icon--large;
+  min-width: $icon--large;
+  min-height: $icon--large;
+}
+
+.participants-reset-state-banner__text {
+  color: $menu-icon-background-color--dark;
+  font-weight: 600;
+}
+
+.participants-reset-state-banner__button {
+  padding: 0 $padding--medium;
+
+  height: 36px;
+  background-color: $menu-icon-background-color--dark;
+  color: $color-white;
+  border: none;
+  border-radius: $rounded--full;
+  font-weight: bold;
+  letter-spacing: $letter-spacing--medium;
+  cursor: pointer;
+
+  &:hover {
+    background-color: $hover-background-dark;
+    color: $color-white;
+  }
+}
+
+@media screen and (min-width: 920px) {
+  // SettingsDialog changes margin here, so we have to do the same
+  .participants-reset-state-banner__container {
+    width: calc(100% + 2 * $settings-dialog-container--sides-large);
+    margin-left: -$settings-dialog-container--sides-large;
+  }
+}
+
 [theme="dark"] {
   .participants__search-input {
     background-color: $color-dark-mode-note;
@@ -241,4 +307,21 @@
       border-color: var(--accent-color);
     }
   }
+
+  .participants-reset-state-banner__container {
+    background-color: $color-dark-mode-note;
+  }
+
+  .participants-reset-state-banner__check-icon,
+  .participants-reset-state-banner__text {
+    color: $color-white;
+  }
+
+  .participants-reset-state-banner__button {
+    background-color: $color-dark-mode;
+
+    &:hover {
+      background-color: $color-dark-one;
+    }
+  }
 }
diff --git a/src/components/SettingsDialog/Participants/Participants.tsx b/src/components/SettingsDialog/Participants/Participants.tsx
index 534ac50b..7e84dc8b 100644
--- a/src/components/SettingsDialog/Participants/Participants.tsx
+++ b/src/components/SettingsDialog/Participants/Participants.tsx
@@ -8,6 +8,7 @@ import {useDebounce} from "utils/hooks/useDebounce";
 import {UserAvatar} from "components/BoardUsers";
 import {ReactComponent as WifiIconDisabled} from "assets/icon-wifi-disabled.svg";
 import {ReactComponent as MagnifyingGlassIcon} from "assets/icon-magnifying-glass.svg";
+import {ReactComponent as ReadyCheckIcon} from "assets/icon-check.svg";
 import "./Participants.scss";
 
 export const Participants = () => {
@@ -21,6 +22,11 @@ export const Participants = () => {
   const isModerator = useAppSelector((state) => state.participants!.self.role === "OWNER" || state.participants!.self.role === "MODERATOR");
   const self = useAppSelector((state) => state.participants!.self);
   const participants = useAppSelector((state) => [state.participants!.self, ...(state.participants?.others ?? [])]);
+  const existsAtLeastOneReadyUser = participants.some((p) => p.ready);
+
+  const resetReadyStateOfAllUsers = () => {
+    participants.forEach((p) => dispatch(Actions.setUserReadyStatus(p.user.id, false)));
+  };
 
   return (
     <section className="settings-dialog__container accent-color__poker-purple">
@@ -109,6 +115,17 @@ export const Participants = () => {
             </li>
           ))}
       </ul>
+      {isModerator && (
+        <footer className={classNames("participants-reset-state-banner__container", {"participants-reset-state-banner__container--is-active": existsAtLeastOneReadyUser})}>
+          <div className="participants-reset-state-banner__icon-and-text">
+            <ReadyCheckIcon className="participants-reset-state-banner__check-icon" />
+            <div className="participants-reset-state-banner__text">{t("Participants.ResetBannerText")}</div>
+          </div>
+          <button className="participants-reset-state-banner__button" onClick={resetReadyStateOfAllUsers}>
+            {t("Participants.ResetBannerButton")}
+          </button>
+        </footer>
+      )}
     </section>
   );
 };
diff --git a/src/components/SettingsDialog/SettingsDialog.scss b/src/components/SettingsDialog/SettingsDialog.scss
index f14963d3..1bcfddf0 100644
--- a/src/components/SettingsDialog/SettingsDialog.scss
+++ b/src/components/SettingsDialog/SettingsDialog.scss
@@ -44,6 +44,8 @@ $settings-dialog-container--bottom: 62px;
   background: $color-white-one;
   width: 100%;
   padding-top: 50px;
+
+  overflow-y: hidden; // so that reset state banner can flow out
 }
 [theme="dark"] .settings-dialog__content {
   background: $color-dark-one;


Test patch:
diff --git a/src/components/SettingsDialog/Participants/__tests__/__snapshots__/Participants.test.tsx.snap b/src/components/SettingsDialog/Participants/__tests__/__snapshots__/Participants.test.tsx.snap
index 921b9c7b..df92b6e0 100644
--- a/src/components/SettingsDialog/Participants/__tests__/__snapshots__/Participants.test.tsx.snap
+++ b/src/components/SettingsDialog/Participants/__tests__/__snapshots__/Participants.test.tsx.snap
@@ -1012,5 +1012,28 @@ exports[`Participants should render filter and participants correctly 1`] = `
       </div>
     </li>
   </ul>
+  <footer
+    class="participants-reset-state-banner__container"
+  >
+    <div
+      class="participants-reset-state-banner__icon-and-text"
+    >
+      <svg
+        class="participants-reset-state-banner__check-icon"
+      >
+        icon-check.svg
+      </svg>
+      <div
+        class="participants-reset-state-banner__text"
+      >
+        Participants.ResetBannerText
+      </div>
+    </div>
+    <button
+      class="participants-reset-state-banner__button"
+    >
+      Participants.ResetBannerButton
+    </button>
+  </footer>
 </section>
 `;