Skip to content

Commit 7d1069d

Browse files
Moved TaskManager component to scommons-react-redux module
1 parent 65321b9 commit 7d1069d

File tree

5 files changed

+426
-10
lines changed

5 files changed

+426
-10
lines changed

project/src/main/scala/definitions/ReactRedux.scala

+3-1
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,9 @@ object ReactRedux extends ScalaJsModule {
2020
)
2121

2222
override val internalDependencies: Seq[ClasspathDep[ProjectReference]] = Seq(
23-
ReactCore.definition
23+
ReactCore.definition,
24+
ReactTest.definition % "test",
25+
ReactTestDom.definition % "test"
2426
)
2527

2628
override val runtimeDependencies: Def.Initialize[Seq[ModuleID]] = Def.setting(Seq(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
package scommons.react.redux.task
2+
3+
import scommons.react._
4+
import scommons.react.hooks._
5+
6+
import scala.scalajs.js
7+
import scala.scalajs.js.{Error, JavaScriptException}
8+
import scala.util.{Failure, Success, Try}
9+
10+
case class TaskManagerProps(startTask: Option[AbstractTask])
11+
12+
/**
13+
* Handles status of running tasks.
14+
*/
15+
object TaskManager extends FunctionComponent[TaskManagerProps] {
16+
17+
var uiComponent: UiComponent[TaskManagerUiProps] = _
18+
19+
var errorHandler: PartialFunction[Try[_], (Option[String], Option[String])] = PartialFunction.empty
20+
21+
private case class TaskManagerState(taskCount: Int = 0,
22+
status: Option[String] = None,
23+
error: Option[String] = None,
24+
errorDetails: Option[String] = None)
25+
26+
protected def render(compProps: Props): ReactElement = {
27+
val props = compProps.wrapped
28+
val (state, setState) = useStateUpdater(() => TaskManagerState())
29+
30+
if (uiComponent == null) {
31+
throw JavaScriptException(Error("TaskManager.uiComponent is not specified"))
32+
}
33+
34+
useEffect({ () =>
35+
props.startTask.foreach { task =>
36+
onTaskStart(setState, task)
37+
}
38+
}, List(props.startTask match {
39+
case None => js.undefined
40+
case Some(task) => task.asInstanceOf[js.Any]
41+
}))
42+
43+
<(uiComponent())(^.wrapped := TaskManagerUiProps(
44+
showLoading = state.taskCount > 0,
45+
status = state.status,
46+
onHideStatus = { () =>
47+
setState(_.copy(status = None))
48+
},
49+
error = state.error,
50+
errorDetails = state.errorDetails,
51+
onCloseErrorPopup = { () =>
52+
setState(_.copy(error = None, errorDetails = None))
53+
}
54+
))()
55+
}
56+
57+
private def onTaskStart(setState: js.Function1[js.Function1[TaskManagerState, TaskManagerState], Unit],
58+
task: AbstractTask): Unit = {
59+
60+
task.onComplete { value: Try[_] =>
61+
onTaskFinish(setState, task, value)
62+
}
63+
64+
setState(s => s.copy(
65+
taskCount = s.taskCount + 1,
66+
status = Some(s"${task.message}...")
67+
))
68+
}
69+
70+
private def onTaskFinish(setState: js.Function1[js.Function1[TaskManagerState, TaskManagerState], Unit],
71+
task: AbstractTask,
72+
value: Try[_]): Unit = {
73+
74+
val durationMillis = System.currentTimeMillis() - task.startTime
75+
val statusMessage = s"${task.message}...Done ${formatDuration(durationMillis)} sec."
76+
77+
def defaultErrorHandler(value: Try[_]): (Option[String], Option[String]) = value match {
78+
case Success(_) => (None, None)
79+
case Failure(e) => (Some(e.toString), Some(printStackTrace(e)))
80+
}
81+
82+
val (error, errorDetails) = errorHandler.applyOrElse(value, defaultErrorHandler)
83+
84+
setState(s => s.copy(
85+
taskCount = s.taskCount - 1,
86+
status = Some(statusMessage),
87+
error = error,
88+
errorDetails = errorDetails
89+
))
90+
}
91+
92+
private[task] def formatDuration(durationMillis: Long): String = {
93+
"%.3f".format(durationMillis / 1000.0)
94+
}
95+
96+
private[task] def printStackTrace(x: Throwable): String = {
97+
val sb = new StringBuilder(x.toString)
98+
val trace = x.getStackTrace
99+
for (t <- trace) {
100+
sb.append("\n\tat&nbsp").append(t)
101+
}
102+
103+
val cause = x.getCause
104+
if (cause != null) {
105+
printStackTraceAsCause(sb, cause, trace)
106+
}
107+
108+
sb.toString
109+
}
110+
111+
/**
112+
* Print stack trace as a cause for the specified stack trace.
113+
*/
114+
private def printStackTraceAsCause(sb: StringBuilder,
115+
cause: Throwable,
116+
causedTrace: Array[StackTraceElement]): Unit = {
117+
118+
// Compute number of frames in common between this and caused
119+
val trace = cause.getStackTrace
120+
var m = trace.length - 1
121+
var n = causedTrace.length - 1
122+
while (m >= 0 && n >= 0 && trace(m) == causedTrace(n)) {
123+
m -= 1
124+
n -= 1
125+
}
126+
127+
val framesInCommon = trace.length - 1 - m
128+
sb.append("\nCaused by: " + cause)
129+
130+
for (i <- 0 to m) {
131+
sb.append("\n\tat&nbsp").append(trace(i))
132+
}
133+
134+
if (framesInCommon != 0) {
135+
sb.append("\n\t...&nbsp").append(framesInCommon).append("&nbspmore")
136+
}
137+
138+
// Recurse if we have a cause
139+
val ourCause = cause.getCause
140+
if (ourCause != null) {
141+
printStackTraceAsCause(sb, ourCause, trace)
142+
}
143+
}
144+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package scommons.react.redux.task
2+
3+
case class TaskManagerUiProps(showLoading: Boolean,
4+
status: Option[String],
5+
onHideStatus: () => Unit,
6+
error: Option[String],
7+
errorDetails: Option[String],
8+
onCloseErrorPopup: () => Unit)

redux/src/test/scala/scommons/react/redux/task/FutureTaskSpec.scala

+4-9
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,12 @@
11
package scommons.react.redux.task
22

3-
import org.scalamock.scalatest.AsyncMockFactory
4-
import org.scalatest.{AsyncFlatSpec, Matchers, Succeeded}
3+
import org.scalatest.Succeeded
4+
import scommons.react.test.dom.AsyncTestSpec
55

6-
import scala.concurrent.{ExecutionContext, Future}
7-
import scala.scalajs.concurrent.JSExecutionContext
6+
import scala.concurrent.Future
87
import scala.util.{Success, Try}
98

10-
class FutureTaskSpec extends AsyncFlatSpec
11-
with Matchers
12-
with AsyncMockFactory {
13-
14-
implicit override val executionContext: ExecutionContext = JSExecutionContext.queue
9+
class FutureTaskSpec extends AsyncTestSpec {
1510

1611
it should "call future.onComplete when onComplete" in {
1712
//given

0 commit comments

Comments
 (0)