From 374ecf324f9d9b6b25b8c76332d740c3c59797fb Mon Sep 17 00:00:00 2001 From: Tom Najdek Date: Tue, 27 Aug 2024 16:29:45 +0200 Subject: [PATCH] Alert if there is an ongoing operation. Fix #478 --- src/js/component/ongoing.jsx | 33 +++++++++++++++++++++-- src/scss/components/_ongoing.scss | 45 ++++++++++++++++++++++++++++--- 2 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/js/component/ongoing.jsx b/src/js/component/ongoing.jsx index 547a26003..2764c67be 100644 --- a/src/js/component/ongoing.jsx +++ b/src/js/component/ongoing.jsx @@ -1,4 +1,5 @@ -import { memo } from 'react'; +import cx from 'classnames'; +import { memo, useCallback, useEffect, useState } from 'react'; import { useSelector } from 'react-redux'; import { pluralize } from '../common/format'; import { Spinner } from 'web-common/components'; @@ -14,9 +15,37 @@ const getMessage = ongoing => { const Ongoing = () => { const processes = useSelector(state => state.ongoing); + const [flash, setFlash] = useState(false); + + const handleOnBeforeUnload = useCallback((ev) => { + if (processes.length > 0) { + // If user cancelled the unload, flash the ongoing pane to + // indicate that there are ongoing operations + setTimeout(() => setFlash(true), 0); + ev.preventDefault(); + } + }, [processes.length]); + + useEffect(() => { + window.addEventListener("beforeunload", handleOnBeforeUnload); + return () => { + window.removeEventListener("beforeunload", handleOnBeforeUnload); + } + }, [handleOnBeforeUnload]); + + useEffect(() => { + if (flash) { + const timer = setTimeout(() => { + setFlash(false); + }, 700); // animation takes 2x330ms + return () => { + clearTimeout(timer); + } + } + }, [flash]); return ( -
+
{ processes.length > 1 ? (
diff --git a/src/scss/components/_ongoing.scss b/src/scss/components/_ongoing.scss index 63c42a7a6..bb688dfeb 100644 --- a/src/scss/components/_ongoing.scss +++ b/src/scss/components/_ongoing.scss @@ -4,16 +4,55 @@ overflow: hidden; transition: height $ongoing-transition; + &.flash { + animation: .33s 2 flash-background; + + @keyframes flash-background { + 0% { + background-color: var(transparent); + + } + + 50% { + background-color: var(--color-shade-10); + } + + 100% { + background-color: var(transparent); + } + } + + .process { + animation: .33s 2 flash-foreground; + + @keyframes flash-foreground { + 0% { + color: $ongoing-color; + } + + 50% { + + color: var(--color-accent); + } + + 100% { + color: $ongoing-color; + } + } + } + } + &:empty { height: 0; } .process { - padding: $space-xs $default-padding-x; - border-top: $border-width solid $ongoing-border-color; - display: flex; align-items: center; + border-top: $border-width solid $ongoing-border-color; color: $ongoing-color; + display: flex; + font-weight: bold; + padding: $space-xs $default-padding-x; } .icon-spin {