diff --git a/src/components/MenuBars/MenuBars.tsx b/src/components/MenuBars/MenuBars.tsx index f16dc18b9b..a115b2b61c 100644 --- a/src/components/MenuBars/MenuBars.tsx +++ b/src/components/MenuBars/MenuBars.tsx @@ -1,17 +1,18 @@ import {animated, useSpring} from "@react-spring/web"; +import classNames from "classnames"; +import {BoardReactionMenu} from "components/BoardReactionMenu/BoardReactionMenu"; +import {AddStickerReaction, ArrowLeft, ArrowRight, Close, GeneralSettings, MarkAsDone, Menu, PresenterMode, RaiseHand, Timer, Voting} from "components/Icon"; +import {TooltipButton} from "components/TooltipButton/TooltipButton"; +import {hotkeyMap} from "constants/hotkeys"; import {useEffect, useLayoutEffect, useRef, useState} from "react"; import {useHotkeys} from "react-hotkeys-hook"; import {useTranslation} from "react-i18next"; import {useDispatch} from "react-redux"; import {useLocation, useNavigate} from "react-router"; -import {Actions} from "store/action"; import {useAppSelector} from "store"; +import {Actions} from "store/action"; import _ from "underscore"; -import classNames from "classnames"; -import {Voting, Timer, RaiseHand, MarkAsDone, AddStickerReaction, GeneralSettings, PresenterMode, Close, ArrowRight, ArrowLeft, Menu} from "components/Icon"; -import {hotkeyMap} from "constants/hotkeys"; -import {TooltipButton} from "components/TooltipButton/TooltipButton"; -import {BoardReactionMenu} from "components/BoardReactionMenu/BoardReactionMenu"; +import {useTimer} from "../../utils/hooks/useTimerLeft"; import "./MenuBars.scss"; export interface MenuBarsProps { @@ -72,7 +73,6 @@ export const MenuBars = ({showPreviousColumn, showNextColumn, onPreviousColumn, }, [location]); const {TOGGLE_TIMER_MENU, TOGGLE_VOTING_MENU, TOGGLE_SETTINGS, TOGGLE_RAISED_HAND, TOGGLE_BOARD_REACTION_MENU, TOGGLE_READY_STATE, TOGGLE_MODERATION} = hotkeyMap; - // State & Functions const state = useAppSelector( (rootState) => ({ @@ -81,6 +81,9 @@ export const MenuBars = ({showPreviousColumn, showNextColumn, onPreviousColumn, hotkeysAreActive: rootState.view.hotkeysAreActive, activeTimer: !!rootState.board.data?.timerEnd, activeVoting: !!rootState.votings.open, + usedVotes: rootState.votes.filter((v) => v.voting === rootState.votings.open?.id).length, + possibleVotes: rootState.votings.open?.voteLimit, + timerEnd: rootState.board.data?.timerEnd, }), _.isEqual ); @@ -188,6 +191,35 @@ export const MenuBars = ({showPreviousColumn, showNextColumn, onPreviousColumn, useHotkeys(TOGGLE_TIMER_MENU, toggleTimerMenu, hotkeyOptionsAdmin, []); useHotkeys(TOGGLE_VOTING_MENU, toggleVotingMenu, hotkeyOptionsAdmin, []); + /** + * Logic for "Mark me as Done" tooltip. + * https://github.com/inovex/scrumlr.io/issues/4269 + */ + const timerExpired = useTimer(state.timerEnd); + const [isReadyTooltipClass, setIsReadyTooltipClass] = useState(""); + /** + * Logic for when a) a timer initially expired b) available votes are used up + * and the "Mark me as Done" tooltip is not open. + */ + const USED_VOTES = state.usedVotes === state.possibleVotes; + const USER_NOT_READY = !state.currentUser.ready; + + useEffect(() => { + let timer: NodeJS.Timeout; + const handleTimeout = () => { + setIsReadyTooltipClass("tooltip-button--content-extended"); + setTimeout(() => setIsReadyTooltipClass(""), 28000); + }; + if ((timerExpired || USED_VOTES) && USER_NOT_READY && (state.activeTimer || state.activeVoting)) { + timer = setTimeout(handleTimeout, 2000); + } + if (!USED_VOTES || !state.activeTimer || !USER_NOT_READY || !state.activeVoting) { + setIsReadyTooltipClass(""); + } + + return () => clearTimeout(timer); + }, [timerExpired, USER_NOT_READY, state.timerEnd, USED_VOTES, state.activeTimer, state.activeVoting]); + return ( <> {/* desktop view */} @@ -202,6 +234,7 @@ export const MenuBars = ({showPreviousColumn, showNextColumn, onPreviousColumn, onClick={toggleReadyState} label={isReady ? t("MenuBars.unmarkAsDone") : t("MenuBars.markAsDone")} icon={MarkAsDone} + className={isReadyTooltipClass} active={isReady} hotkeyKey={TOGGLE_READY_STATE.toUpperCase()} /> diff --git a/src/components/MenuBars/__tests__/MenuBars.test.tsx b/src/components/MenuBars/__tests__/MenuBars.test.tsx index a4e5f59711..029305ba8a 100644 --- a/src/components/MenuBars/__tests__/MenuBars.test.tsx +++ b/src/components/MenuBars/__tests__/MenuBars.test.tsx @@ -1,10 +1,13 @@ -import {render} from "testUtils"; import {MenuBars} from "components/MenuBars"; import {Provider} from "react-redux"; -import getTestStore from "utils/test/getTestStore"; import {MockStoreEnhanced} from "redux-mock-store"; +import {render} from "testUtils"; +import getTestStore from "utils/test/getTestStore"; +import {useTimer} from "../../../utils/hooks/useTimerLeft"; import getTestParticipant from "../../../utils/test/getTestParticipant"; +jest.mock("../../../utils/hooks/useTimerLeft"); + const createMenuBars = (store: MockStoreEnhanced) => ( {}} onPreviousColumn={() => {}} /> @@ -12,7 +15,13 @@ const createMenuBars = (store: MockStoreEnhanced) => ( ); describe("MenuBars", () => { + beforeEach(() => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + }); + test("should match snapshot", () => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + const store = getTestStore({ participants: { self: getTestParticipant({role: "MODERATOR"}), @@ -24,6 +33,8 @@ describe("MenuBars", () => { }); test("should render both user- and admin-menu for moderators", () => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + const store = getTestStore({ participants: { self: getTestParticipant({role: "MODERATOR"}), @@ -36,6 +47,8 @@ describe("MenuBars", () => { }); test("should only render user-menu for participants", () => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + const store = getTestStore({ participants: { self: getTestParticipant({role: "PARTICIPANT"}), @@ -47,3 +60,44 @@ describe("MenuBars", () => { expect(container.getElementsByClassName("user-menu").length).toBe(1); }); }); + +describe("Mark me as Done Tooltip Logic", () => { + beforeEach(() => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + }); + + it("Does not expand the tooltip if the timer is not expired", () => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + const store = getTestStore({ + participants: { + self: getTestParticipant(), + others: [], + }, + }); + const {asFragment} = render(createMenuBars(store)); + expect(asFragment()).toMatchSnapshot(); + }); + + it("Does not expand the tooltip if the possibleVotes !== usedVotes", () => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + const store = getTestStore({ + participants: { + self: getTestParticipant(), + others: [], + }, + }); + const {asFragment} = render(createMenuBars(store)); + expect(asFragment()).toMatchSnapshot(); + }); + it("Expand the tooltip everytime a potential ready state in voting is fulfilled.", () => { + (useTimer as jest.Mock).mockReturnValue({timerExpired: false}); + const store = getTestStore({ + participants: { + self: getTestParticipant(), + others: [], + }, + }); + const {asFragment} = render(createMenuBars(store)); + expect(asFragment()).toMatchSnapshot(); + }); +}); diff --git a/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap b/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap index 39e6539d7a..6ac6cc7d3c 100644 --- a/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap +++ b/src/components/MenuBars/__tests__/__snapshots__/MenuBars.test.tsx.snap @@ -1,5 +1,1103 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`Mark me as Done Tooltip Logic Does not expand the tooltip if the possibleVotes !== usedVotes 1`] = ` + + + + +`; + +exports[`Mark me as Done Tooltip Logic Does not expand the tooltip if the timer is not expired 1`] = ` + + + + +`; + +exports[`Mark me as Done Tooltip Logic Expand the tooltip everytime a potential ready state in voting is fulfilled. 1`] = ` + + + + +`; + exports[`MenuBars should match snapshot 1`] = `